Compare commits

..

71 Commits

Author SHA1 Message Date
Timo 3fe8ca18e4 chore: release v2.1.1 2026-06-04 15:33:06 +02:00
GitHub Action f0ccc0c082 chore(release): update versions to v2.1.0 [skip ci] 2026-06-04 13:20:10 +00:00
Timo d59fc4777d chore: release v2.1.0 with stabilization and audit fixes 2026-06-04 15:18:57 +02:00
Timo 3ad2459558 docs: update TRANSLATION.md with new languages and flags 2026-06-04 15:02:27 +02:00
Timo 839f4d0761 docs: write changelog for v2.1.0 and update README.md 2026-06-04 15:01:14 +02:00
Timo 8838bf20b7 style: add flag emojis to all language selectors 2026-06-04 15:00:10 +02:00
Timo 9d849a2996 feat: add 7 new languages to extension and website, bump version to 2.1.0 2026-06-04 14:59:14 +02:00
Timo 6d5661ea45 feat: update icons for extension and website, automate responsive logo sizes in build pipeline 2026-06-04 14:46:55 +02:00
Koala 886550408a docs: align PRIVACY.md and website datenschutz with the server-only tracking statements 2026-06-03 13:46:11 +02:00
Koala 28694c22bb docs: expand privacy and no-logs statement in main README.md 2026-06-03 13:43:35 +02:00
Koala a2a56f2b17 fix(docs): remove unsupported type: gauge from json_exporter.example.yml 2026-06-03 13:34:19 +02:00
Koala 742876e415 docs: add json_exporter configuration example for prometheus monitoring 2026-06-03 13:08:41 +02:00
KoalaDev bcd956a8db Update README for v2.0.8 release announcement 2026-06-03 12:12:17 +02:00
GitHub Action 1e329c0c52 chore(release): update versions to v2.0.8 [skip ci] 2026-06-03 10:11:57 +00:00
Koala a7481d42a2 fix(extension): resolve language selector UI overwrite and dynamic version reporting 2026-06-03 12:09:39 +02:00
GitHub Action f9577aace9 chore(release): update versions to v2.0.7 [skip ci] 2026-06-03 09:58:23 +00:00
Koala ad33720053 feat(server): add DEBUG_LOGGING environment variable to suppress verbose console output 2026-06-03 11:57:58 +02:00
Koala fa3341cc8e docs(server): quote values in env.example and set empty token 2026-06-03 11:51:49 +02:00
GitHub Action b526e9287b chore(release): update versions to v2.0.6 [skip ci] 2026-06-03 09:45:12 +00:00
Koala a6be6b2670 perf(server): optimize failedAuthAttempts LRU eviction to O(1) 2026-06-03 11:44:06 +02:00
Koala b51e66d824 docs: add modularization point to ROADMAP 2026-06-03 11:40:04 +02:00
Koala e034882975 docs: update release link in README to v2.0.5 2026-06-03 11:38:07 +02:00
Koala e53a93829e docs: enforce no-manual-version-bumping rule in AI_INIT and SYNC_GUIDE 2026-06-03 11:36:09 +02:00
GitHub Action e3dbf2b8a2 chore(release): update versions to v2.0.5 [skip ci] 2026-06-03 09:34:22 +00:00
Koala 595ea297f5 chore(release): release v2.0.5 2026-06-03 11:33:54 +02:00
GitHub Action a948780745 chore(release): update versions to v2.0.4 [skip ci] 2026-06-03 09:07:20 +00:00
Koala 8c899a6469 Update changelog for v2.0.4 2026-06-03 11:06:44 +02:00
Koala 4f4cdcd8c0 Cache health responses server-side 2026-06-03 11:04:45 +02:00
Koala 602be3a724 Tighten health endpoint hardening 2026-06-03 10:59:08 +02:00
Koala bb7bd21102 Update release assets for v2.0.3 2026-06-03 10:51:47 +02:00
GitHub Action 543bfe074d chore(release): update versions to v2.0.3 [skip ci] 2026-06-03 08:49:25 +00:00
Koala 81c50eff16 Harden room discovery and health metrics 2026-06-03 10:48:49 +02:00
Koala bf0fb5741f fix: version.json path for all non-English locales
Changed condition from lang === 'de' to lang === 'en' so that all
non-English language subdirectories (fr, es, pt-BR, ru, de) correctly
resolve version.json via ../version.json instead of only German.
2026-06-02 14:31:13 +02:00
Timo 6d2355f404 Store Assets added 2026-06-02 09:26:20 +02:00
Koala 732d585273 docs: add v2.0.2 changelog and update README badge 2026-06-02 08:23:49 +02:00
GitHub Action 666808f876 chore(release): update versions to v2.0.2 [skip ci] 2026-06-02 06:17:21 +00:00
Koala 3ec9812265 fix: use server-trusted peerId in relay payload to prevent spoofing 2026-06-02 08:17:08 +02:00
Koala 64f3c5eefd fix: use future-proof regex for Amazon domain matching 2026-06-02 08:06:35 +02:00
Koala 3fe074b308 fix: use orteil.dashnet.org instead of cookieclicker.com in blacklist 2026-06-02 08:04:31 +02:00
Koala a06cca8b9d fix: apply boundary-safe hostname matching to remaining platform and blacklist checks 2026-06-02 08:02:47 +02:00
KoalaDev c4fc3ab53b Merge pull request #4 from Shik3i/alert-autofix-3
Potential fix for code scanning alert no. 3: Incomplete URL substring sanitization
2026-06-02 07:55:08 +02:00
KoalaDev 3f3fea58ed Merge pull request #5 from Shik3i/alert-autofix-2
Potential fix for code scanning alert no. 2: DOM text reinterpreted as HTML
2026-06-02 07:54:51 +02:00
KoalaDev 014169f84e Potential fix for code scanning alert no. 2: DOM text reinterpreted as HTML
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-02 07:51:01 +02:00
KoalaDev 9c61abee03 Potential fix for code scanning alert no. 3: Incomplete URL substring sanitization
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-02 07:50:35 +02:00
KoalaDev 2f11c60307 Merge pull request #2 from Shik3i/dependabot/npm_and_yarn/server/npm_and_yarn-e5a46ec0e1
chore(deps): bump ws from 8.18.3 to 8.20.1 in /server in the npm_and_yarn group across 1 directory
2026-06-02 07:41:19 +02:00
KoalaDev 27301f8746 Merge pull request #3 from Shik3i/dependabot/npm_and_yarn/server/npm_and_yarn-05b1f1d78b
chore(deps): bump qs from 6.15.1 to 6.15.2 in /server in the npm_and_yarn group across 1 directory
2026-06-02 07:40:26 +02:00
dependabot[bot] 030b839b12 chore(deps): bump qs
Bumps the npm_and_yarn group with 1 update in the /server directory: [qs](https://github.com/ljharb/qs).


Updates `qs` from 6.15.1 to 6.15.2
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.15.1...v6.15.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.15.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 05:39:05 +00:00
dependabot[bot] 5c805bcafa chore(deps): bump ws
Bumps the npm_and_yarn group with 1 update in the /server directory: [ws](https://github.com/websockets/ws).


Updates `ws` from 8.18.3 to 8.20.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.3...8.20.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.20.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 05:39:00 +00:00
Koala eb83d9148e Update contact email in CODE_OF_CONDUCT 2026-06-02 07:23:38 +02:00
Koala 290360387f Add CODE_OF_CONDUCT and PULL_REQUEST_TEMPLATE
- Add CODE_OF_CONDUCT.md based on Contributor Covenant 2.1
- Add .github/PULL_REQUEST_TEMPLATE.md with structured sections
- Link CODE_OF_CONDUCT in README.md and CONTRIBUTING.md
2026-06-02 07:22:37 +02:00
Koala c56e404c13 Overhaul issue templates
- Rewrite bug_report.md with professional structure and Copy Logs requirement
- Rewrite feature_request.md to focus on problem, use cases, and benefits
2026-06-02 07:19:49 +02:00
KoalaDev e519ea2302 Update issue templates 2026-06-02 07:16:17 +02:00
Koala 5a75bae8d0 mobile: nav container padding auf 0.5rem reduziert 2026-06-02 06:43:57 +02:00
Koala fc3f4f38ff mobile: Header noch weiter nach links gerückt
- Nav Container Padding auf Mobile reduziert (0.75rem statt 2rem)
- Logo-Area Gap auf 0.25rem reduziert
- Icon auf 40x40 verkleinert
- Schriftgröße auf 1.1rem reduziert
- Vertical margin vom Icon entfernt
2026-06-02 06:41:46 +02:00
Koala f69da7542e mobile: Sprachauswahl immer sichtbar, Hamburger entfernt
- Sprachauswahl aus nav-links in neuen nav-right Container verschoben
- nav-right ist auf Mobile rechtsbündig und immer sichtbar
- Hamburger-Menü entfernt (Links sind Same-Page-Anchors)
- Logo-Bereich auf Mobile kompakter (kleineres Icon, weniger Gap)
- Lang-Selector auf Mobile kompakter (kleinere Icons, weniger Padding)
- Gleiches Restructuring für join.html, impressum.html, datenschutz.html
2026-06-02 06:35:39 +02:00
Timo aba05060b9 feat: add localization comparison row to website comparison table 2026-06-01 17:11:55 +02:00
Timo 0b986e6e13 fix: build-time VERSION injection via build.js instead of JS runtime 2026-06-01 16:27:45 +02:00
Timo b23ce8ee7d fix: dynamic mockup version from version.json instead of hardcoded 1.9.3 2026-06-01 16:25:18 +02:00
Timo 3195bd0089 docs: move older changes into v2.0.0 where they belong 2026-06-01 16:21:35 +02:00
Timo 1c1778d265 docs: remove older changelog entries - attribution unclear 2026-06-01 16:20:36 +02:00
Timo 0262d9c1ad docs: fix changelog dates to match git tag timestamps 2026-06-01 16:19:11 +02:00
Timo f23c329709 docs: move release notice to top of README 2026-06-01 16:18:25 +02:00
Timo 78a165d368 docs: clarify Copy Debug Report description in v2.0.0 2026-06-01 16:16:40 +02:00
Timo 63b5ef0ffd docs: remove website-only entries from changelog, keep extension+server only 2026-06-01 16:16:23 +02:00
Timo a26b07cb5c docs: highlight extension i18n as biggest v2.0.0 feature 2026-06-01 16:15:32 +02:00
Timo a445759dd7 docs: add CHANGELOG.md, link from README, version badge -> clickable changelog 2026-06-01 16:14:30 +02:00
Timo 1e883d8ee4 chore: update assets - add icon folder, remove old SOCIAL_PREVIEW.png 2026-06-01 16:03:40 +02:00
GitHub Action be574e0e25 chore(release): update versions to v2.0.1 [skip ci] 2026-06-01 13:59:31 +00:00
Timo ebf3178e32 feat: multi-video overview in debug report
- GET_VIDEO_STATE now returns allVideos[] with summary per video element
- Copy report shows markdown table when multiple videos on page
- Table includes: resolution, muted, paused, readyState, duration, and marks selected target
- Helps diagnose Prime Video scenarios where preview vs main video both exist
2026-06-01 15:58:31 +02:00
Timo ba96cf2765 fix: prioritize largest video element, fix history action field in debug report
- findVideo() now scores all video elements by size + unmuted + duration
- Fixes Prime Video selecting 0x0 placeholder instead of actual player
- Fix history entries showing '?' by using h.action instead of h.event/h.type
- Update Prime Video status in TESTED_SERVICES.md to partial support
2026-06-01 15:57:11 +02:00
GitHub Action d026ed891a chore(release): update versions to v2.0.0 [skip ci] 2026-06-01 13:50:26 +00:00
167 changed files with 15356 additions and 1327 deletions
+56
View File
@@ -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.).
+31
View File
@@ -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).
+45
View File
@@ -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.js` and updated relevant docs
- [ ] No new warnings, secrets, or hardcoded credentials introduced
### Additional Context
<!-- Screenshots, migration notes, performance data, etc. -->
+2 -1
View File
@@ -106,7 +106,8 @@ Before starting any task, committing, or pushing, you **MUST** run `git pull --r
### 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.
+146
View File
@@ -0,0 +1,146 @@
# KoalaSync Changelog
All notable changes to the KoalaSync browser extension and relay server.
---
## [v2.1.1] — 2026-06-04
### Changed
- Added progressive enhancement using `appearance: base-select` to support styling of country flag emojis in expanded language selectors on newer Chromium browsers.
---
## [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.
+131
View File
@@ -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
+2
View File
@@ -2,6 +2,8 @@
Thanks for your interest in improving KoalaSync. All contributions are welcome — from bug reports and translations to core protocol changes.
Please note that by participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md).
---
## Ways to Contribute
+3 -1
View File
@@ -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.
- **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
+6 -2
View File
@@ -12,11 +12,13 @@
<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>
<p align="center"><a href="CHANGELOG.md"><b>New v2.1.1 Release!</b> — See what's changed</a></p>
<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>
### 🌟 Why KoalaSync?
* **🛡️ Security-First**: Volatile RAM-based relay with built-in brute-force protection and zero-persistence architecture.
* **🛡️ 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.
@@ -95,7 +97,7 @@ To verify your relay is reachable from outside, visit `https://your-domain.com`
### 🌐 Localization & Translations
Both the official KoalaSync website and the **v2.0 Browser Extension** feature full dynamic localization:
- **Available Languages**: Support is included for 6 languages: English (`en`), German (`de`), French (`fr`), Spanish (`es`), Portuguese (Brazil) (`pt-BR`), and Russian (`ru`).
- **Available Languages**: Support is included for 13 languages: English (`en`), German (`de`), French (`fr`), Spanish (`es`), Portuguese (Brazil) (`pt-BR`), Russian (`ru`), Italian (`it`), Polish (`pl`), Turkish (`tr`), Dutch (`nl`), Japanese (`ja`), Korean (`ko`), and European Portuguese (`pt`).
- **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](website/TRANSLATION.md) guide for step-by-step instructions on how to audit, refine, or add new languages.
@@ -104,10 +106,12 @@ Both the official KoalaSync website and the **v2.0 Browser Extension** feature f
### 📖 Documentation & Links
- **[CHANGELOG.md](CHANGELOG.md)**: Full version history for the extension and relay server.
- **[TESTED_SERVICES.md](TESTED_SERVICES.md)**: Detailed compatibility matrix of tested streaming platforms and known limitations.
- **[TRANSLATION.md](website/TRANSLATION.md)**: Translation and localization guide for contributors.
- **[PRIVACY.md](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.
- **[SECURITY.md](SECURITY.md)**: Disclosure policy and security practices.
+3 -2
View File
@@ -73,8 +73,9 @@ Encrypt sensitive findings with our PGP key (available on request).
KoalaSync's security is grounded in its architecture:
- **RAM-only relay**: No database, no persistent logs. All session data evaporates on disconnect.
- **bcrypt room passwords**: Plaintext passwords never stored. Brute-force protection: 5 attempts → 15-minute IP lockout.
- **Rate limiting**: Connection rate (IP-based, 60s window) and event rate (per-socket, 10s window).
- **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.
+1 -1
View File
@@ -11,7 +11,7 @@ This document tracks which streaming platforms and media servers have been teste
| **Jellyfin** | ✅ Full | ✅ Full | ✅ Full | Self-hosted. Full HTML5 player access. |
| **Plex** | Not tested | Not tested | Not tested | Community reports indicate compatibility via HTML5 player mode. |
| **Disney+** | Not tested | Not tested | Not tested | Widevine DRM may restrict title detection similar to Netflix. |
| **Prime Video** | ❌ None | ❌ None | ❌ | Extension cannot detect video elements. Play/Pause do not work. Media title detection fails. DRM and custom player prevent script access. |
| **Prime Video** | ⚠️ Partial | ⚠️ Partial | ❌ | Video elements detected (2 on page, picks larger one). Playback state + time readable. However, the preview/trailer video may be selected instead of the main content. Play/Pause commands may not reach the correct player. Title detection from MediaSession API may work for some content. |
| **HBO Max / Max** | Not tested | Not tested | Not tested | — |
| **Crunchyroll** | Not tested | Not tested | Not tested | — |
| **Vimeo** | Not tested | Not tested | Not tested | — |
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+33
View File
@@ -0,0 +1,33 @@
Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.
KoalaSync: The Privacy-First Watch Party Extension
Synchronize movies, series, and videos in real-time with friends.
100% free, secure, and no account required.
Whether you're binge-watching Netflix, streaming from your own self-hosted Emby, Jellyfin, or Plex servers, or watching on virtually any other website with a video player — KoalaSync keeps everyone perfectly in sync. Lightweight, open-source, and built for absolute data sovereignty.
✨ CORE FEATURES
• No Account Needed: Just create a room and share the link. No emails, no passwords, no hassle. A random nickname is assigned automatically, or pick your own.
• Universal HTML5 Sync: Flawlessly synchronizes play, pause, and seeking on Netflix, Jellyfin, Emby, YouTube, and almost any website hosting a standard HTML5 <video> tag.
• Smart Binge-Watching (Auto-Sync): When a new episode loads, KoalaSync automatically pauses playback in a smart lobby until everyone's video is ready. No spoilers, no one left behind.
• One-Click Invitations: Generate a smart invite link. Once clicked, the extension instantly configures room credentials and server settings for your guests.
• Zero-Latency Protocol: Powered by a custom WebSocket architecture designed for real-time coordination without buffering loops.
🛡️ UNCOMPROMISING PRIVACY
No data collection. No compromises.
• 100% Anonymous: Zero tracking, zero analytics, zero telemetry.
• Ready Out-Of-The-Box: No technical knowledge needed. Install and start watching immediately using the official public server.
• RAM-Only Public Server: The official relay server operates entirely in volatile RAM. No databases, no logs, no watch history — nothing persists.
• Self-Hostable: Take total control by deploying a private relay server in seconds via Docker. Optional, never required.
🚀 HOW IT WORKS
1. Install KoalaSync and click "Create Room" to spin up a secure lobby.
2. Copy the invite link and send it to your friends.
3. Open your favorite streaming site and set the video tab as the active target.
4. Press play — everyone stays perfectly in sync.
💻 OPEN SOURCE
KoalaSync was built by a solo developer who needed a fast, secure way to watch movies with friends — and builds tools worth using. The code is fully transparent under the MIT license: audit it, fork it, improve it. 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
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+2 -1
View File
@@ -8,7 +8,8 @@ services:
- 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=50 # Maximum number of peers allowed per room
- 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
+2 -1
View File
@@ -10,7 +10,8 @@ services:
- 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=50 # Maximum number of peers allowed per room
- 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
+1 -1
View File
@@ -58,7 +58,7 @@ KoalaSync uses a megaphone routing approach to minimize server logic:
## 7. Security & Stability
- **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 DoS. Real client IP extracted via `x-forwarded-for` header behind proxies/CDNs.
- **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.
+2 -2
View File
@@ -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!".
+7
View File
@@ -13,3 +13,10 @@ Dieses Dokument erfasst zukünftige technische Pläne und Optimierungen für das
* **Geplante Lösung**:
- Umstellung auf eine echte, $O(1)$-basierte LRU-Cache-Datenstruktur (z. B. doppelt verkettete Liste in Kombination mit einer Map).
- Alternativ: Ein vereinfachtes zeitbasiertes Ablauf-Verfahren oder ein schrittweises Löschen von Segmenten (Chunk-Eviction), um Blockaden des Main-Threads vollständig auszuschließen.
### 2. Aufteilung großer JavaScript-Dateien (> 800 Zeilen) in kleinere Module
* **Kategorie**: Wartbarkeit / AI-Kontext-Optimierung
* **Hintergrund**: Einige Kern-Dateien wie `background.js` und `popup.js` sind stark angewachsen und überschreiten 800 Zeilen. Dies erschwert das manuelle Debugging und verbraucht unnötig viel Kontextfenster bei AI-Modellen.
* **Geplante Lösung**:
- Strukturierte Aufteilung der Logik in separate, fokussierte Module (z. B. UI-Renderer, Message-Router, Storage-Manager, Socket-Client).
- Nutzung von ES-Modulen zur sauberen Strukturierung und besseren Wiederverwendbarkeit.
+1 -1
View File
@@ -31,7 +31,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.
+2 -1
View File
@@ -36,7 +36,8 @@ export default [
URL: "readonly",
URLSearchParams: "readonly",
WebSocket: "readonly",
self: "readonly"
self: "readonly",
process: "readonly"
}
},
rules: {
Binary file not shown.
+121 -33
View File
@@ -1,6 +1,6 @@
import { EVENTS, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, APP_VERSION, EPISODE_LOBBY_TIMEOUT, FORCE_SYNC_TIMEOUT } from './shared/constants.js';
import { EVENTS, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, EPISODE_LOBBY_TIMEOUT, FORCE_SYNC_TIMEOUT } from './shared/constants.js';
import { generateUsername } from './shared/names.js';
import { loadLocale, getMessage, SUPPORTED_LANGUAGES } from './i18n.js';
import { loadLocale, getMessage, getSystemLanguage } from './i18n.js';
// --- State Management ---
@@ -56,7 +56,7 @@ function ensureState() {
'logs', 'history', 'currentRoom', 'lastActionState',
'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks',
'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'reconnectAttempts', 'currentTabId', 'currentTabTitle',
'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount'
'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount', 'roomIdleSince', 'lastContentHeartbeatAt'
], (data) => {
clearTimeout(storageTimeout);
if (data.expectedAcksCount !== undefined) expectedAcksCount = data.expectedAcksCount;
@@ -80,6 +80,8 @@ function ensureState() {
if (data.reconnectFailed !== undefined) reconnectFailed = data.reconnectFailed;
if (data.reconnectStartTime) reconnectStartTime = data.reconnectStartTime;
if (data.reconnectAttempts !== undefined) reconnectAttempts = data.reconnectAttempts;
if (data.roomIdleSince !== undefined) roomIdleSince = data.roomIdleSince;
if (data.lastContentHeartbeatAt !== undefined) lastContentHeartbeatAt = data.lastContentHeartbeatAt;
// Recover Force Sync Timeout
if (data.forceSyncDeadline) {
@@ -139,9 +141,12 @@ let reconnectStartTime = null;
let reconnectFailed = false;
let reconnectAttempts = 0;
let currentServerUrl = null;
let roomIdleSince = null;
let lastContentHeartbeatAt = null;
const MAX_RECONNECT_ATTEMPTS = 20;
const _RECONNECT_BASE_DELAY = 500;
const _RECONNECT_MAX_DELAY = 5000;
const ROOM_IDLE_AUTO_LEAVE_MS = 2 * 60 * 60 * 1000;
// Force Sync Coordination
let isForceSyncInitiator = false;
@@ -304,6 +309,8 @@ function forceDisconnect() {
isNamespaceJoined = false;
isForceSyncInitiator = false;
expectedAcksCount = 0;
roomIdleSince = null;
lastContentHeartbeatAt = null;
forceSyncAcks.clear();
eventQueue = [];
chrome.storage.session.set({
@@ -312,7 +319,9 @@ function forceDisconnect() {
forceSyncDeadline: null,
expectedAcksCount: 0,
eventQueue: [],
episodeLobby: null
episodeLobby: null,
roomIdleSince: null,
lastContentHeartbeatAt: null
}).catch(() => {});
if (currentRoom) {
currentRoom.peers = [];
@@ -322,6 +331,63 @@ function forceDisconnect() {
broadcastConnectionStatus('disconnected');
}
function persistRoomIdleState() {
chrome.storage.session.set({ roomIdleSince, lastContentHeartbeatAt }).catch(() => {});
}
function markRoomUseful() {
roomIdleSince = null;
lastContentHeartbeatAt = Date.now();
persistRoomIdleState();
}
function markRoomPotentiallyIdle() {
if (!currentRoom) {
roomIdleSince = null;
lastContentHeartbeatAt = null;
persistRoomIdleState();
return;
}
if (!roomIdleSince) {
roomIdleSince = Date.now();
persistRoomIdleState();
}
}
function clearTargetTabForIdle() {
currentTabId = null;
currentTabTitle = null;
lastContentHeartbeatAt = null;
if (currentRoom) {
roomIdleSince = Date.now();
}
chrome.storage.session.set({ currentTabId, currentTabTitle, roomIdleSince, lastContentHeartbeatAt }).catch(() => {});
updateBadgeStatus();
}
async function leaveRoomAfterIdleGrace(reason) {
if (!currentRoom) return;
emit(EVENTS.LEAVE_ROOM, { peerId });
currentRoom = null;
currentTabId = null;
currentTabTitle = null;
roomIdleSince = null;
lastContentHeartbeatAt = null;
clearEpisodeLobbyState();
await chrome.storage.session.set({
currentRoom: null,
currentTabId: null,
currentTabTitle: null,
roomIdleSince: null,
lastContentHeartbeatAt: null,
episodeLobby: null
}).catch(() => {});
await chrome.storage.sync.set({ roomId: '', password: '' }).catch(() => {});
addLog(reason, 'info');
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
updateBadgeStatus();
}
async function connect() {
if (isConnecting) return;
isConnecting = true;
@@ -391,7 +457,7 @@ async function connect() {
url.pathname = '/socket.io/';
url.searchParams.set('EIO', '4');
url.searchParams.set('transport', 'websocket');
url.searchParams.set('version', APP_VERSION);
url.searchParams.set('version', chrome.runtime.getManifest().version);
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
socket = new WebSocket(url.toString());
@@ -482,12 +548,15 @@ async function connect() {
socket.onerror = () => {
broadcastConnectionStatus('disconnected');
addLog('WebSocket Error: Connection failed', 'error');
const logType = reconnectAttempts > 1 ? 'error' : 'warn';
addLog('WebSocket Error: Connection failed', logType);
};
} catch (e) {
isConnecting = false;
addLog(e.message, 'error');
const logType = reconnectAttempts > 1 ? 'error' : 'warn';
const errMsg = (e && e.message) ? e.message : String(e || 'Unknown connection error');
addLog(errMsg, logType);
broadcastConnectionStatus('disconnected');
scheduleReconnect();
}
@@ -522,11 +591,7 @@ function showNotification(senderName, action) {
chrome.storage.sync.get(['browserNotifications', 'locale'], async (settings) => {
if (!settings.browserNotifications) return;
let lang = settings.locale;
if (!lang) {
const systemLang = (navigator.language || chrome.i18n.getUILanguage()).split('-')[0];
lang = SUPPORTED_LANGUAGES.includes(systemLang) ? systemLang : 'en';
}
const lang = settings.locale || getSystemLanguage();
await loadLocale(lang);
let labelKey = '';
@@ -625,9 +690,14 @@ function addToHistory(action, senderId) {
// --- Event Handlers ---
function handleServerEvent(event, data) {
if (!data) {
addLog(`Ignored server event ${event} due to empty payload`, 'warn');
return;
}
switch (event) {
case EVENTS.ROOM_DATA:
currentRoom = data;
markRoomPotentiallyIdle();
if (currentRoom && Array.isArray(currentRoom.peers)) {
currentRoom.peers = currentRoom.peers.map(p => typeof p === 'object' ? createPeerData(p) : { peerId: p, username: null, tabTitle: null, mediaTitle: null, playbackState: null, currentTime: null, volume: null, muted: null, lastHeartbeat: Date.now() });
@@ -692,11 +762,7 @@ function handleServerEvent(event, data) {
broadcastConnectionStatus('disconnected');
addLog(`Server Error: ${data.message}`, 'error');
chrome.storage.sync.get(['locale'], async (settings) => {
let lang = settings.locale;
if (!lang) {
const systemLang = (navigator.language || chrome.i18n.getUILanguage()).split('-')[0];
lang = SUPPORTED_LANGUAGES.includes(systemLang) ? systemLang : 'en';
}
const lang = settings.locale || getSystemLanguage();
await loadLocale(lang);
chrome.notifications.create(`error_${Date.now()}`, {
type: 'basic',
@@ -1030,11 +1096,7 @@ function cancelEpisodeLobby(reason) {
chrome.storage.sync.get(['browserNotifications', 'locale'], async (settings) => {
if (!settings.browserNotifications) return;
let lang = settings.locale;
if (!lang) {
const systemLang = (navigator.language || chrome.i18n.getUILanguage()).split('-')[0];
lang = SUPPORTED_LANGUAGES.includes(systemLang) ? systemLang : 'en';
}
const lang = settings.locale || getSystemLanguage();
await loadLocale(lang);
const reasonKey = reasonKeys[reason];
@@ -1149,8 +1211,7 @@ function _routeToContentInternal(tabId, action, payload, actionTimestamp, comman
}).catch(err => {
if (retries >= 3) {
addLog(`Content Script not responding in tab ${tabId} after ${retries} retries`, 'warn');
currentTabId = null;
updateBadgeStatus();
clearTargetTabForIdle();
return;
}
if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
@@ -1164,8 +1225,7 @@ function _routeToContentInternal(tabId, action, payload, actionTimestamp, comman
});
} else {
addLog(`Content Script not responding in tab ${tabId}`, 'warn');
currentTabId = null;
updateBadgeStatus();
clearTargetTabForIdle();
}
});
}
@@ -1181,6 +1241,15 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
connect();
}
} else if (currentRoom) {
const now = Date.now();
const heartbeatAge = lastContentHeartbeatAt ? (now - lastContentHeartbeatAt) : Infinity;
if (!currentTabId || heartbeatAge > 45000) {
markRoomPotentiallyIdle();
}
if (roomIdleSince && Date.now() - roomIdleSince >= ROOM_IDLE_AUTO_LEAVE_MS) {
await leaveRoomAfterIdleGrace('Left room after 2 hours without a selected video heartbeat.');
return;
}
// Heartbeat Logic: Always include identity metadata
const settings = await getSettings();
emit(EVENTS.PEER_STATUS, {
@@ -1227,6 +1296,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
});
async function handleAsyncMessage(message, sender, sendResponse) {
if (!message) return;
await ensureState();
if (message.type === 'CONNECT') {
@@ -1276,7 +1346,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
reconnectSlowMode: reconnectFailed,
roomId: currentRoom ? currentRoom.roomId : null,
serverUrl: currentServerUrl,
version: APP_VERSION,
version: chrome.runtime.getManifest().version,
protocolVersion: PROTOCOL_VERSION,
roomPassword: currentRoom ? currentRoom.password : null
});
@@ -1284,6 +1354,9 @@ async function handleAsyncMessage(message, sender, sendResponse) {
emit(EVENTS.LEAVE_ROOM, { peerId });
currentRoom = null;
currentTabId = null;
currentTabTitle = null;
roomIdleSince = null;
lastContentHeartbeatAt = null;
updateBadgeStatus();
@@ -1297,6 +1370,10 @@ async function handleAsyncMessage(message, sender, sendResponse) {
chrome.storage.session.set({
currentRoom: null,
currentTabId: null,
currentTabTitle: null,
roomIdleSince: null,
lastContentHeartbeatAt: null,
isForceSyncInitiator: false,
forceSyncAcks: [],
forceSyncDeadline: null,
@@ -1376,15 +1453,19 @@ async function handleAsyncMessage(message, sender, sendResponse) {
localSeq++;
chrome.storage.session.set({ localSeq });
updateLastAction(message.action, 'You', timestamp);
lastActionState.targetTime = message.payload?.targetTime !== undefined ? message.payload.targetTime : message.payload?.currentTime;
const payload = message.payload || {};
lastActionState.targetTime = payload.targetTime !== undefined ? payload.targetTime : payload.currentTime;
if (storageInitialized) chrome.storage.session.set({ lastActionState });
message.payload.actionTimestamp = timestamp;
message.payload.seq = localSeq;
payload.actionTimestamp = timestamp;
payload.seq = localSeq;
message.payload = payload;
// Local Reactive Update
updateLocalPeerState(peerId, {
playbackState: message.action === EVENTS.PLAY ? 'playing' : (message.action === EVENTS.PAUSE ? 'paused' : undefined),
currentTime: message.payload.currentTime !== undefined ? message.payload.currentTime : (message.payload.targetTime !== undefined ? message.payload.targetTime : undefined)
currentTime: payload.currentTime !== undefined ? payload.currentTime : (payload.targetTime !== undefined ? payload.targetTime : undefined)
});
if (message.action === EVENTS.FORCE_SYNC_PREPARE) {
@@ -1489,6 +1570,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
updateBadgeStatus();
}
markRoomUseful();
getSettings().then(settings => {
const statusPayload = { ...message.payload, peerId, username: settings.username, tabTitle: currentTabTitle };
emit(EVENTS.PEER_STATUS, statusPayload);
@@ -1516,7 +1598,11 @@ async function handleAsyncMessage(message, sender, sendResponse) {
} else if (message.type === 'SET_TARGET_TAB') {
currentTabId = message.tabId;
currentTabTitle = message.tabTitle;
chrome.storage.session.set({ currentTabId, currentTabTitle });
lastContentHeartbeatAt = null;
if (currentRoom) {
roomIdleSince = Date.now();
}
chrome.storage.session.set({ currentTabId, currentTabTitle, roomIdleSince, lastContentHeartbeatAt });
updateBadgeStatus();
if (currentTabId) {
@@ -1642,7 +1728,9 @@ chrome.tabs.onRemoved.addListener(async (tabId) => {
const wasInRoom = !!currentRoom;
currentTabId = null;
currentTabTitle = null;
chrome.storage.session.set({ currentTabId: null, currentTabTitle: null });
lastContentHeartbeatAt = null;
roomIdleSince = Date.now();
chrome.storage.session.set({ currentTabId: null, currentTabTitle: null, roomIdleSince, lastContentHeartbeatAt });
updateBadgeStatus();
addLog('Target tab closed.', 'warn');
+3 -1
View File
@@ -10,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',
@@ -17,11 +18,12 @@ 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 detail = { success: msg.success, message: msg.message };
// Firefox MV3 content scripts run in an isolated world. When dispatching
+67 -23
View File
@@ -78,21 +78,40 @@
}
// --- Helper: find the best video element on the page ---
// Prefers larger, visible videos over tiny preview/trailer elements.
function findVideo(root = document) {
const video = root.querySelector('video');
if (video) return video;
// Optimize: scan only potential player, video, media, and stream hosts by matching typical keywords (case-insensitive)
// or common custom element tags. This prevents recursive scanning of thousands of standard DOM nodes (div, span, a, etc.)
// while guaranteeing 100% airtight compatibility with all video web components in the wild.
const candidates = Array.from(root.querySelectorAll('video'));
// Scan likely media hosts even when light-DOM videos exist; many players
// expose a tiny preview/ad video outside Shadow DOM and the real player inside.
const potentialHosts = root.querySelectorAll('[id*="player" i], [class*="player" i], [id*="video" i], [class*="video" i], [id*="media" i], [class*="media" i], [id*="stream" i], [class*="stream" i], ytd-player, netflix-player, emby-player, jellyfin-player, video-player');
for (const el of potentialHosts) {
if (el.shadowRoot) {
const found = findVideo(el.shadowRoot);
if (found) return found;
if (found) candidates.push(found);
}
}
return null;
if (candidates.length === 0) return null;
// Multiple videos found → pick the best one
if (candidates.length === 1) return candidates[0];
let best = null;
let bestScore = -1;
for (const v of candidates) {
if (v.tagName !== 'VIDEO') continue;
// Score: visible area + bonus for unmuted + bonus for longer duration
const area = (v.videoWidth || v.offsetWidth || 0) * (v.videoHeight || v.offsetHeight || 0);
const unmutedBonus = v.muted ? 0 : 100000;
const durationBonus = (v.duration && isFinite(v.duration) ? v.duration : 0) * 100;
const score = area + unmutedBonus + durationBonus;
if (score > bestScore) {
bestScore = score;
best = v;
}
}
return best;
}
// --- Episode Auto-Sync: Detection ---
@@ -242,8 +261,8 @@
try {
const host = window.location.hostname.toLowerCase();
const isYouTube = host.includes('youtube.com');
const isTwitch = host.includes('twitch.tv');
const isYouTube = host === 'youtube.com' || host.endsWith('.youtube.com');
const isTwitch = host === 'twitch.tv' || host.endsWith('.twitch.tv');
if (isYouTube) {
const ytButton = document.querySelector('.ytp-play-button');
@@ -325,6 +344,7 @@
// Listen for commands from background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!message) return;
if (message.action === 'get_current_time') {
const video = findVideo();
sendResponse({ currentTime: video ? video.currentTime : null });
@@ -347,7 +367,7 @@
if (isDifferentEpisode(senderTitle, myTitle)) {
reportLog(`Episode mismatch: sender="${senderTitle || '?'}" vs mine="${myTitle || '?'}" — skipping ${action}. Disable "Auto-Sync next Episode" in settings if this causes issues.`, 'warn');
if (action !== EVENTS.FORCE_SYNC_PREPARE && action !== EVENTS.FORCE_SYNC_EXECUTE) {
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp, commandSenderId: message.commandSenderId });
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp, commandSenderId: message.commandSenderId }).catch(() => {});
}
return;
}
@@ -376,7 +396,11 @@
_setSuppress('paused');
_setSuppress('seek');
video.pause();
video.currentTime = payload.targetTime;
try {
video.currentTime = payload.targetTime;
} catch (e) {
reportLog(`Force Sync Seek Error: ${e.message}`, 'error');
}
pollSeekReady(payload.targetTime).then((ready) => {
chrome.runtime.sendMessage({ type: 'FORCE_SYNC_ACK' }).catch(() => {});
if (ready) {
@@ -436,15 +460,15 @@
const platform = (() => {
const h = window.location.hostname.toLowerCase();
if (h.includes('youtube.com')) return 'YouTube';
if (h.includes('twitch.tv')) return 'Twitch';
if (h.includes('netflix.com')) return 'Netflix';
if (h.includes('primevideo.com') || h.includes('amazon.')) return 'Prime Video';
if (h.includes('disneyplus.com')) return 'Disney+';
if (h.includes('hulu.com')) return 'Hulu';
if (h.includes('max.com') || h.includes('hbomax.com')) return 'Max/HBO';
if (h.includes('vimeo.com')) return 'Vimeo';
if (h.includes('dailymotion.com')) return 'Dailymotion';
if (h === 'youtube.com' || h.endsWith('.youtube.com')) return 'YouTube';
if (h === 'twitch.tv' || h.endsWith('.twitch.tv')) return 'Twitch';
if (h === 'netflix.com' || h.endsWith('.netflix.com')) return 'Netflix';
if (h === 'primevideo.com' || h.endsWith('.primevideo.com') || /(^|\.)amazon\.(com\.[a-z]{2}|co\.[a-z]{2}|[a-z]{2,})$/.test(h)) return 'Prime Video';
if (h === 'disneyplus.com' || h.endsWith('.disneyplus.com')) return 'Disney+';
if (h === 'hulu.com' || h.endsWith('.hulu.com')) return 'Hulu';
if (h === 'hbomax.com' || h.endsWith('.hbomax.com') || h === 'max.com' || h.endsWith('.max.com')) return 'Max/HBO';
if (h === 'vimeo.com' || h.endsWith('.vimeo.com')) return 'Vimeo';
if (h === 'dailymotion.com' || h.endsWith('.dailymotion.com')) return 'Dailymotion';
return 'Generic';
})();
@@ -468,6 +492,24 @@
return false;
})();
// Build multi-video summary for debug reports
const allVideos = [];
const allVideoEls = document.querySelectorAll('video');
for (let i = 0; i < allVideoEls.length; i++) {
const v = allVideoEls[i];
allVideos.push({
index: i,
width: v.videoWidth || v.offsetWidth || 0,
height: v.videoHeight || v.offsetHeight || 0,
muted: v.muted,
paused: v.paused,
duration: (v.duration && isFinite(v.duration)) ? Math.round(v.duration) : 0,
readyState: v.readyState,
src: (v.currentSrc || v.src || '').substring(0, 80),
selected: v === video
});
}
if (video) {
const dataAttributes = {};
if (video.attributes) {
@@ -522,7 +564,8 @@
metadata,
videoCount,
inShadowDom,
platform
platform,
allVideos
});
} else {
sendResponse({
@@ -530,6 +573,7 @@
videoCount,
inShadowDom,
platform,
allVideos,
url: window.location.href,
pageTitle: document.title,
metadata: (navigator.mediaSession && navigator.mediaSession.metadata) ? {
@@ -583,7 +627,7 @@
mediaTitle: mediaTitle,
timestamp: Date.now()
}
});
}).catch(() => {});
// Trigger proactive heartbeat to push stabilized state
scheduleProactiveHeartbeat();
+54 -7
View File
@@ -1,8 +1,10 @@
// extension/i18n.js
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru'];
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'];
export const DEFAULT_LANGUAGE = 'en';
let activeDictionary = {};
const dictionaryCache = {};
let currentLanguage = null;
/**
* Resolves, loads, and merges the target language with the English baseline fallback.
@@ -11,13 +13,30 @@ let activeDictionary = {};
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
const enResponse = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
const enDict = await enResponse.json();
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;
}
@@ -26,15 +45,27 @@ export async function loadLocale(langCode) {
const targetDict = await targetResponse.json();
// Airtight Fallback Merge: target overrides en, missing elements fallback to en
activeDictionary = Object.assign({}, enDict, targetDict);
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 {
const enResponse = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
activeDictionary = await enResponse.json();
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;
}
}
}
@@ -46,7 +77,7 @@ export async function loadLocale(langCode) {
* @returns {string} Translated string or the key itself
*/
export function getMessage(key, placeholders = null) {
let msg = activeDictionary[key] || key;
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);
@@ -87,3 +118,19 @@ export function translateDOM() {
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;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+3
View File
@@ -33,6 +33,9 @@
"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",
+3
View File
@@ -33,6 +33,9 @@
"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",
+3
View File
@@ -33,6 +33,9 @@
"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",
+3
View File
@@ -33,6 +33,9 @@
"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",
+183
View File
@@ -0,0 +1,183 @@
{
"LANG_CODE": "it",
"HTML_CLASS": "lang-it",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Stanza",
"TAB_ROOM_TOOLTIP": "Impostazioni della stanza e connessione",
"TAB_SYNC": "Sincronizza",
"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 ospitato autonomamente",
"PLACEHOLDER_SERVER_URL": "wss://tuo-server:3000",
"LABEL_ROOM_ID": "ID Stanza",
"LABEL_ROOM_ID_TOOLTIP": "L'identificatore univoco per la tua stanza di sincronizzazione",
"PLACEHOLDER_ROOM_ID": "Inserisci ID Stanza",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "L'ID univoco della stanza a cui vuoi unirti",
"LABEL_PASSWORD": "Password (Opzionale)",
"LABEL_PASSWORD_TOOLTIP": "Password opzionale per limitare l'accesso alla stanza",
"PLACEHOLDER_PASSWORD": "Password della stanza (opzionale)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Password per la stanza (lascia vuoto se nessuna)",
"BTN_JOIN_ROOM": "Entra / Crea Stanza",
"BTN_JOIN_ROOM_TOOLTIP": "Connettiti alla stanza",
"LABEL_PUBLIC_ROOMS": "Stanze Pubbliche",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Elenco delle stanze pubblicamente disponibili su questo server",
"BTN_REFRESH": "AGGIORNA",
"BTN_REFRESH_TOOLTIP": "Aggiorna l'elenco delle stanze pubbliche",
"PUBLIC_ROOMS_REFRESHING": "Aggiornamento...",
"BTN_REFRESH_COOLDOWN": "ATTENDI {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "L'aggiornamento dell'elenco è in pausa. Riprova tra {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Aggiornamento stanze pubbliche. Prossimo aggiornamento disponibile tra {seconds}s.",
"LABEL_ACTIVE_ROOM": "Stanza Attiva",
"LABEL_ACTIVE_ROOM_TOOLTIP": "La stanza a cui sei attualmente connesso",
"ACTIVE_ROOM_NONE": "NESSUNA",
"ACTIVE_SERVER_OFFICIAL": "Server Ufficiale",
"LABEL_INVITE_LINK": "Link di Invito",
"LABEL_INVITE_LINK_TOOLTIP": "Condividi questo link con gli amici per farli entrare",
"LABEL_PEERS_IN_ROOM": "Partecipanti nella Stanza",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Altri utenti attualmente connessi a questa stanza",
"NO_PEERS_CONNECTED": "Nessun partecipante connesso",
"BTN_LEAVE_ROOM": "Lascia Stanza",
"LABEL_SELECT_VIDEO": "Seleziona Video",
"LABEL_SELECT_VIDEO_TOOLTIP": "Scegli la scheda del browser contenente il video da sincronizzare",
"OPTION_SELECT_TAB": "-- Seleziona una Scheda --",
"LABEL_REMOTE_CONTROL": "Controllo Remoto",
"BTN_COPY_INVITE": "📋 Link di Invito",
"BTN_COPY_INVITE_TOOLTIP": "Copia Link di Invito",
"BTN_PLAY": "▶ Riproduci",
"BTN_PLAY_TOOLTIP": "Invia un comando di riproduzione a tutti",
"BTN_PAUSE": "⏸ Pausa",
"BTN_PAUSE_TOOLTIP": "Invia un comando di pausa a tutti",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Forza la sincronizzazione di tutti gli utenti",
"OPTION_JUMP_TO_OTHERS": "Salto dagli Altri",
"OPTION_JUMP_TO_ME": "Salto da Me",
"OPTION_JUMP_MODE_TOOLTIP": "Scegli il target di sincronizzazione",
"LABEL_LAST_ACTIVITY": "Stato Ultima Attività",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Mostra il comando più recente di riproduzione, pausa o ricerca",
"NO_RECENT_COMMANDS": "Nessun comando recente",
"LOBBY_HEADER": "LOBBY EPISODIO",
"LOBBY_WAITING_FOR": "🎬 In attesa di: \"{title}\"",
"LOBBY_WAITING_PEERS": "In attesa dei partecipanti...",
"BTN_SKIP_PLAY": "Salta e riproduci comunque",
"BTN_SKIP_PLAY_TOOLTIP": "Annulla la lobby e riproduci comunque",
"LOBBY_CONNECT_FIRST": "Connettiti prima a una stanza",
"LOBBY_CONNECT_FIRST_DESC": "Devi unirti a una stanza tramite un link di invito o crearne una nuova per sincronizzare i video.",
"BTN_CREATE_ROOM_ALT": "Crea Nuova Stanza",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Crea una nuova stanza casuale ed entra",
"LABEL_USERNAME": "Tuo Nome Utente",
"LABEL_USERNAME_TOOLTIP": "Il nome utente aiuta gli altri a identificarti.",
"PLACEHOLDER_USERNAME": "Koala Anonimo",
"LABEL_HIDE_CLUTTER": "Nascondi Schede Inutili",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra le schede senza video e i domini non correlati per mantenere pulito l'elenco",
"LABEL_AUTO_SYNC_NEXT": "Auto-Sync Prossimo Episodio",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automaticamente e attende tutti i partecipanti al cambio episodio, poi si avvia in sincro.",
"LABEL_AUTO_COPY_INVITE": "Copia Automatica Invito",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia automaticamente il link di invito negli appunti quando crei una nuova stanza.",
"LABEL_NOTIFICATIONS": "Notifiche del Browser",
"LABEL_NOTIFICATIONS_TOOLTIP": "Mostra le notifiche di sistema quando qualcuno entra/esce o riproduce/mette in pausa.",
"LABEL_LANGUAGE": "Lingua Applicazione",
"LABEL_LANGUAGE_TOOLTIP": "Scegli la lingua preferita per l'estensione",
"LABEL_TROUBLESHOOTING": "Risoluzione Problemi",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Strumenti per risolvere problemi di connessione",
"BTN_REGEN_ID": "Rigenera ID Peer",
"BTN_REGEN_ID_TOOLTIP": "Rigenera il tuo ID interno e riconnettiti",
"REGEN_ID_DESC": "Usa questo se riscontri errori di \"Identità Duplicata\".",
"LABEL_CONN_STATUS": "Stato Connessione",
"LABEL_CONN_STATUS_TOOLTIP": "Stato corrente della connessione WebSocket",
"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 per la condivisione",
"LABEL_VIDEO_DEBUG": "Info Debug Video",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Dettagli tecnici sull'elemento video attualmente selezionato",
"VIDEO_DEBUG_EMPTY": "Nessuna scheda selezionata o video rilevato.",
"LABEL_HISTORY": "Cronologia Azioni Completa",
"LABEL_HISTORY_TOOLTIP": "Registro cronologico di tutti i comandi di sincronizzazione nella stanza",
"HISTORY_EMPTY": "Ancora nessuna attività",
"LABEL_LOGS": "Log (Ultimi 50)",
"LABEL_LOGS_TOOLTIP": "Log di connessione tecnica per il debug",
"BTN_CLEAR": "PULISCI",
"BTN_CLEAR_TOOLTIP": "Cancella l'output del 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 in perfetta sincronia, non importa dove ti trovi. Facciamo un rapido tour!",
"ONBOARDING_2_TITLE": "1. Crea una Stanza",
"ONBOARDING_2_TEXT": "Inizia qui. Crea una stanza e condividi il link di invito con i tuoi amici.",
"ONBOARDING_3_TITLE": "2. Seleziona Video",
"ONBOARDING_3_TEXT": "Naviga qui per selezionare il video che vuoi sincronizzare. Riproduci, metti in pausa e cerca: tutti rimangono sincronizzati.",
"ONBOARDING_4_TITLE": "3. Personalizza",
"ONBOARDING_4_TEXT": "Scegli un nome utente divertente così i tuoi amici sanno chi sei.",
"ONBOARDING_5_TITLE": "Tutto pronto!",
"ONBOARDING_5_TEXT": "È ora di prendere i popcorn. Buona visione insieme!",
"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 e riprova.",
"ERR_NO_PEERS_TIME": "Nessun altro peer con un orario noto. Passa a 'Salto da Me'.",
"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": "Lobby dell'episodio saltata.",
"TOAST_LOBBY_SKIP_FAILED": "Impossibile saltare la lobby.",
"TOAST_LOGS_COPIED": "Copiato!",
"TOAST_PEER_JOINED": "{name} è entrato nella stanza",
"TOAST_PEER_LEFT": "{name} ha lasciato la stanza",
"TOAST_PEER_ACTION": "{name} ha {action}",
"STATUS_CONNECTED": "Connesso",
"STATUS_RECONNECTING": "Riconnessione...",
"STATUS_CONNECTING": "Connessione in corso...",
"STATUS_FAILED": "Fallito",
"STATUS_DISCONNECTED": "Disconnesso",
"BTN_STATE_JOINING": "🚀 Entrando...",
"BTN_STATE_RECONNECTING": "🔄 Riconnessione...",
"BTN_STATE_PLAYING": "▶ Riproduzione...",
"BTN_STATE_PAUSING": "⏸ In pausa...",
"BTN_STATE_SYNCING_GROUP": "Sincronizzazione al gruppo ({time})...",
"BTN_STATE_SYNCING": "Sincronizzazione...",
"BTN_STATE_SYNCED": "✅ Sincronizzato!",
"NOTIF_PLAY": "avviato la riproduzione",
"NOTIF_PAUSE": "messo in pausa la riproduzione",
"NOTIF_SEEK": "cercato nel video",
"NOTIF_FORCE_PREPARE": "avviato la sincronizzazione forzata",
"NOTIF_FORCE_EXECUTE": "sincronizzato tutti",
"DEBUG_NO_TAB": "Nessuna scheda di destinazione selezionata.",
"DEBUG_COMM_FAIL": "Impossibile comunicare con il video della scheda.",
"EMPTY_PEERS_TITLE": "Ancora nessun partecipante",
"EMPTY_PEERS_HINT": "Condividi il tuo link di invito per iniziare",
"EMPTY_HISTORY_TITLE": "Ancora nessuna attività",
"EMPTY_HISTORY_HINT": "Riproduci, metti in pausa o cerca per vedere la cronologia",
"EMPTY_LOGS_TITLE": "Nessun log",
"EMPTY_LOGS_HINT": "Gli eventi di connessione appariranno qui",
"EMPTY_ROOMS_TITLE": "Nessuna stanza attiva",
"EMPTY_ROOMS_HINT": "Crea una stanza o aggiorna per trovare quelle pubbliche",
"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 — Sincronizzazione Episodio Fallita",
"NOTIF_LOBBY_CANCEL_MSG": "Auto-sync annullato: {reason}. Potrebbe essere necessaria una sincronizzazione manuale.",
"LOBBY_CANCEL_TIMEOUT": "Timeout",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Timeout (ripristinato)",
"LOBBY_CANCEL_PEERS_LEFT": "Tutti gli altri partecipanti hanno lasciato la stanza",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — non tutti i partecipanti hanno caricato l'episodio",
"LOBBY_CANCEL_USER": "Annullato dall'utente",
"NOTIF_ERROR_TITLE": "Errore KoalaSync"
}
+183
View File
@@ -0,0 +1,183 @@
{
"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": "現在このルームに接続している他のユーザー",
"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重複)エラーが表示される場合に使用します。",
"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": "切断されました",
"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エラー"
}
+183
View File
@@ -0,0 +1,183 @@
{
"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": "Refreschi",
"BTN_REFRESH_TOOLTIP": "공개 방 목록 새로고침",
"PUBLIC_ROOMS_REFRESHING": "새로고침 중...",
"BTN_REFRESH_COOLDOWN": "{seconds}초 대기",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "방 목록 새로고침 대기 중입니다. {seconds}초 후에 다시 시도하세요.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "공개 방 새로고침 중. {seconds}초 후 새로고침 가능.",
"LABEL_ACTIVE_ROOM": "활성 방",
"LABEL_ACTIVE_ROOM_TOOLTIP": "현재 연결된 방",
"ACTIVE_ROOM_NONE": "없음",
"ACTIVE_SERVER_OFFICIAL": "공식 서버",
"LABEL_INVITE_LINK": "초대 링크",
"LABEL_INVITE_LINK_TOOLTIP": "친구들이 참여할 수 있도록 이 링크를 공유하세요",
"LABEL_PEERS_IN_ROOM": "방의 참여자",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "현재 이 방에 연결되어 있는 다른 사용자들",
"NO_PEERS_CONNECTED": "연결된 참여자가 없습니다",
"BTN_LEAVE_ROOM": "방 나가기",
"LABEL_SELECT_VIDEO": "비디오 선택",
"LABEL_SELECT_VIDEO_TOOLTIP": "동기화할 비디오가 포함된 브라우저 탭을 선택하세요",
"OPTION_SELECT_TAB": "-- 탭 선택 --",
"LABEL_REMOTE_CONTROL": "원격 제어",
"BTN_COPY_INVITE": "📋 초대 링크",
"BTN_COPY_INVITE_TOOLTIP": "초대 링크 복사",
"BTN_PLAY": "▶ 재생",
"BTN_PLAY_TOOLTIP": "모든 사람에게 재생 명령 전송",
"BTN_PAUSE": "⏸ 일시정지",
"BTN_PAUSE_TOOLTIP": "모든 사람에게 일시정지 명령 전송",
"BTN_SYNC": "⚡ 동기화",
"BTN_SYNC_TOOLTIP": "모든 사용자의 강제 동기화",
"OPTION_JUMP_TO_OTHERS": "다른 사람에게 이동",
"OPTION_JUMP_TO_ME": "나에게 이동",
"OPTION_JUMP_MODE_TOOLTIP": "동기화 대상 선택",
"LABEL_LAST_ACTIVITY": "최근 활동 상태",
"LABEL_LAST_ACTIVITY_TOOLTIP": "가장 최근의 재생, 일시정지, 탐색 명령을 표시합니다",
"NO_RECENT_COMMANDS": "최근 명령 없음",
"LOBBY_HEADER": "에피소드 로비",
"LOBBY_WAITING_FOR": "🎬 대기 중: \"{title}\"",
"LOBBY_WAITING_PEERS": "참여자 대기 중...",
"BTN_SKIP_PLAY": "건너뛰고 재생",
"BTN_SKIP_PLAY_TOOLTIP": "로비를 취소하고 재생합니다",
"LOBBY_CONNECT_FIRST": "먼저 방에 연결하세요",
"LOBBY_CONNECT_FIRST_DESC": "비디오를 동기화하려면 초대 링크를 통해 방에 참여하거나 새 방을 만들어야 합니다.",
"BTN_CREATE_ROOM_ALT": "새 방 만들기",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "새로운 무작위 방을 만들고 참여",
"LABEL_USERNAME": "사용자 이름",
"LABEL_USERNAME_TOOLTIP": "사용자 이름은 다른 사람들이 귀하를 식별하는 데 도움이 됩니다.",
"PLACEHOLDER_USERNAME": "익명의 코알라",
"LABEL_HIDE_CLUTTER": "복잡한 탭 숨기기",
"LABEL_HIDE_CLUTTER_TOOLTIP": "목록을 깔끔하게 유지하기 위해 비디오가 없는 탭과 관련 없는 도메인을 필터링합니다",
"LABEL_AUTO_SYNC_NEXT": "다음 에피소드 자동 동기화",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "에피소드가 변경되면 자동으로 일시정지하고 모든 참여자를 기다린 후 함께 동기화하여 시작합니다.",
"LABEL_AUTO_COPY_INVITE": "초대 링크 자동 복사",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "새 방을 만들 때 초대 링크를 클립보드에 자동으로 복사합니다.",
"LABEL_NOTIFICATIONS": "브라우저 알림",
"LABEL_NOTIFICATIONS_TOOLTIP": "누군가 참여/퇴장하거나 재생/일시정지할 때 시스템 알림을 표시합니다.",
"LABEL_LANGUAGE": "앱 언어",
"LABEL_LANGUAGE_TOOLTIP": "선호하는 확장 프로그램 언어를 선택하세요",
"LABEL_TROUBLESHOOTING": "문제 해결",
"LABEL_TROUBLESHOOTING_TOOLTIP": "연결 문제 해결을 위한 도구",
"BTN_REGEN_ID": "피어 ID 재생성",
"BTN_REGEN_ID_TOOLTIP": "내부 ID를 재생성하고 다시 연결합니다",
"REGEN_ID_DESC": "\"중복된 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": "연kel 중...",
"STATUS_FAILED": "실패",
"STATUS_DISCONNECTED": "연결 끊김",
"BTN_STATE_JOINING": "🚀 참여 중...",
"BTN_STATE_RECONNECTING": "🔄 재연결 중...",
"BTN_STATE_PLAYING": "▶ 재생 중...",
"BTN_STATE_PAUSING": "⏸ 일시정지 중...",
"BTN_STATE_SYNCING_GROUP": "그룹에 동기화 중 ({time})...",
"BTN_STATE_SYNCING": "동기화 중...",
"BTN_STATE_SYNCED": "✅ 동기화됨!",
"NOTIF_PLAY": "재생을 시작했습니다",
"NOTIF_PAUSE": "재생을 일시정지했습니다",
"NOTIF_SEEK": "비디오를 탐색했습니다",
"NOTIF_FORCE_PREPARE": "강제 동기화를 시작했습니다",
"NOTIF_FORCE_EXECUTE": "모든 사용자를 동기화했습니다",
"DEBUG_NO_TAB": "대상 탭이 선택되지 않았습니다.",
"DEBUG_COMM_FAIL": "탭 비디오와 통신할 수 없습니다.",
"EMPTY_PEERS_TITLE": "참여자 없음",
"EMPTY_PEERS_HINT": "시작하려면 초대 링크를 공유하세요",
"EMPTY_HISTORY_TITLE": "활동 없음",
"EMPTY_HISTORY_HINT": "기록을 보려면 재생, 일시정지 또는 탐색을 해보세요",
"EMPTY_LOGS_TITLE": "로그 없음",
"EMPTY_LOGS_HINT": "연결 이벤트가 여기에 표시됩니다",
"EMPTY_ROOMS_TITLE": "활성 방 없음",
"EMPTY_ROOMS_HINT": "방을 만들거나 새로고침하여 공개 방을 찾으세요",
"LABEL_YOU": "나",
"ONBOARDING_DONE": "완료!",
"LABEL_LOBBY_PEER_READY": "준비",
"LABEL_LOBBY_PEER_LOADING": "로딩 중...",
"LABEL_PASSWORD_PROTECTED": "비밀번호 보호됨",
"LABEL_PEERS_COUNT": "{count}명",
"LABEL_CUSTOM_SERVER": "사용자 정의 서버",
"BTN_STATE_CREATING": "🚀 방 만드는 중...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — 에피소드 동기화 실패",
"NOTIF_LOBBY_CANCEL_MSG": "자동 동기화 취소됨: {reason}. 수동 동기화가 필요할 수 있습니다.",
"LOBBY_CANCEL_TIMEOUT": "시간 초과",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "시간 초과 (복구됨)",
"LOBBY_CANCEL_PEERS_LEFT": "다른 모든 피어가 나갔습니다",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "시간 초과 — 모든 피어가 에피소드를 로드하지 못했습니다",
"LOBBY_CANCEL_USER": "사용자에 의해 취소됨",
"NOTIF_ERROR_TITLE": "KoalaSync 오류"
}
+183
View File
@@ -0,0 +1,183 @@
{
"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",
"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'.",
"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",
"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 cmmuniceren 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"
}
+183
View File
@@ -0,0 +1,183 @@
{
"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",
"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.",
"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",
"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"
}
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "ATUALIZAR",
"BTN_REFRESH_TOOLTIP": "Atualizar a lista de salas públicas",
"PUBLIC_ROOMS_REFRESHING": "Atualizando...",
"BTN_REFRESH_COOLDOWN": "AGUARDE {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "A lista de salas está em espera. Tente novamente em {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Atualizando salas públicas. Próxima atualização em {seconds}s.",
"LABEL_ACTIVE_ROOM": "Sala ativa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "A sala à qual você está conectado no momento",
"ACTIVE_ROOM_NONE": "NENHUMA",
+183
View File
@@ -0,0 +1,183 @@
{
"LANG_CODE": "pt",
"HTML_CLASS": "lang-pt",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Sala",
"TAB_ROOM_TOOLTIP": "Definições da sala e ligação",
"TAB_SYNC": "Sincronização",
"TAB_SYNC_TOOLTIP": "Controlo de sincronização de vídeo e ações remotas",
"TAB_SETTINGS": "Definições",
"TAB_SETTINGS_TOOLTIP": "Preferências da extensão",
"TAB_STATUS": "Estado",
"TAB_STATUS_TOOLTIP": "Diagnóstico avanzado e registos",
"BTN_CREATE_ROOM": "+ Criar Nova Sala",
"MANUAL_CONNECT_HEADER": "Ligação Manual / Avançado",
"LABEL_SERVER": "Servidor",
"BTN_SERVER_OFFICIAL": "Oficial",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Usar o servidor oficial fiável",
"BTN_SERVER_CUSTOM": "Personalizado",
"BTN_SERVER_CUSTOM_TOOLTIP": "Ligar ao seu próprio servidor auto-hospedado",
"PLACEHOLDER_SERVER_URL": "wss://o-seu-servidor:3000",
"LABEL_ROOM_ID": "ID da Sala",
"LABEL_ROOM_ID_TOOLTIP": "O identificador exclusivo para a sua sala de sincronização",
"PLACEHOLDER_ROOM_ID": "Introduzir ID da Sala",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "O ID exclusivo da sala à qual deseja aderir",
"LABEL_PASSWORD": "Palavra-passe (Opcional)",
"LABEL_PASSWORD_TOOLTIP": "Palavra-passe opcional para restringir o acesso à sala",
"PLACEHOLDER_PASSWORD": "Palavra-passe da sala (opcional)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Palavra-passe para la sala (deixar em branco se não houver)",
"BTN_JOIN_ROOM": "Entrar / Criar Sala",
"BTN_JOIN_ROOM_TOOLTIP": "Ligar à sala",
"LABEL_PUBLIC_ROOMS": "Salas Públicas",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Lista de salas públicas disponíveis neste servidor",
"BTN_REFRESH": "ATUALIZAR",
"BTN_REFRESH_TOOLTIP": "Atualizar a lista de salas públicas",
"PUBLIC_ROOMS_REFRESHING": "A atualizar...",
"BTN_REFRESH_COOLDOWN": "AGUARDE {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "A atualização da lista de salas está em espera. Tente novamente em {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "A atualizar salas públicas. Próxima atualização disponível em {seconds}s.",
"LABEL_ACTIVE_ROOM": "Sala Ativa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "A sala à qual está atualmente ligado",
"ACTIVE_ROOM_NONE": "NENHUMA",
"ACTIVE_SERVER_OFFICIAL": "Servidor Oficial",
"LABEL_INVITE_LINK": "Link de Invito",
"LABEL_INVITE_LINK_TOOLTIP": "Partilhar este link com os amigos para que possam entrar",
"LABEL_PEERS_IN_ROOM": "Participantes na Sala",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Outros utilizadores attualmente ligados a questa sala",
"NO_PEERS_CONNECTED": "Nenhum partecipante ligado",
"BTN_LEAVE_ROOM": "Sair della Sala",
"LABEL_SELECT_VIDEO": "Selecionar Vídeo",
"LABEL_SELECT_VIDEO_TOOLTIP": "Escolher o separador do navegador que contém o vídeo a sincronizar",
"OPTION_SELECT_TAB": "-- Selecionar um Separador --",
"LABEL_REMOTE_CONTROL": "Controlo Remoto",
"BTN_COPY_INVITE": "📋 Link de Invito",
"BTN_COPY_INVITE_TOOLTIP": "Copiar Link de Invito",
"BTN_PLAY": "▶ Reproduzir",
"BTN_PLAY_TOOLTIP": "Enviar um comando de Reproduzir para todos",
"BTN_PAUSE": "⏸ Pausa",
"BTN_PAUSE_TOOLTIP": "Enviar um comando de Pausa para todos",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Forçar todos os utilizadores a sincronizarem-se",
"OPTION_JUMP_TO_OTHERS": "Ir para os Outros",
"OPTION_JUMP_TO_ME": "Ir para Mim",
"OPTION_JUMP_MODE_TOOLTIP": "Escolher o destino de sincronização",
"LABEL_LAST_ACTIVITY": "Estado da Última Atividade",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Mostra o comando mais recente de reproduzir, pausa ou busca",
"NO_RECENT_COMMANDS": "Nenhum comando recente",
"LOBBY_HEADER": "LOBBY DE EPISÓDIO",
"LOBBY_WAITING_FOR": "🎬 A aguardar: \"{title}\"",
"LOBBY_WAITING_PEERS": "A aguardar participantes...",
"BTN_SKIP_PLAY": "Ignorar e reproduzir mesmo assim",
"BTN_SKIP_PLAY_TOOLTIP": "Cancelar o lobby e reproduzir mesmo assim",
"LOBBY_CONNECT_FIRST": "Ligar-se primeiro a uma sala",
"LOBBY_CONNECT_FIRST_DESC": "Precisa de entrar numa sala através de um link de convite ou criar uma nova para sincronizar vídeos.",
"BTN_CREATE_ROOM_ALT": "Criar Nova Sala",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Criar uma nova sala aleatória e entrar nela",
"LABEL_USERNAME": "O seu Nome de Utilizador",
"LABEL_USERNAME_TOOLTIP": "O nome de utilizador ajuda os outros a identificá-lo.",
"PLACEHOLDER_USERNAME": "Coala Anónimo",
"LABEL_HIDE_CLUTTER": "Ocultar Separadores Desnecessários",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra separadores sem vídeo e domínios não relacionados para manter a lista limpa",
"LABEL_AUTO_SYNC_NEXT": "Sincronização Automática do Próximo Episódio",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automaticamente e aguarda por todos os participantes quando um episódio muda, iniciando depois a reprodução em sincronia.",
"LABEL_AUTO_COPY_INVITE": "Copiar Automaticamente o Link de Convite",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia automaticamente o link de convite para a área de transferência ao criar uma nova sala.",
"LABEL_NOTIFICATIONS": "Notificações do Navegador",
"LABEL_NOTIFICATIONS_TOOLTIP": "Mostra notificações do sistema quando alguém entra/sai ou reproduz/pausa.",
"LABEL_LANGUAGE": "Idioma da App",
"LABEL_LANGUAGE_TOOLTIP": "Escolha o seu idioma de preferência para a extensão",
"LABEL_TROUBLESHOOTING": "Resolução de Problemas",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Ferramentas para corrigir problemas de ligação",
"BTN_REGEN_ID": "Regerar ID do Peer",
"BTN_REGEN_ID_TOOLTIP": "Regerar o seu ID interno e voltar a ligar",
"REGEN_ID_DESC": "Use isto se vir erros de \"Identidade Duplicata\".",
"LABEL_CONN_STATUS": "Estado da Ligação",
"LABEL_CONN_STATUS_TOOLTIP": "Estado atual da ligação WebSocket",
"CONN_STATUS_DISCONNECTED": "Desligado",
"BTN_RETRY": "TENTAR NOVAMENTE",
"BTN_RETRY_TOOLTIP": "Tentar voltar a ligar ao servidor",
"BTN_COPY_LOGS": "Copiar Registos",
"BTN_COPY_LOGS_TOOLTIP": "Copiar registos para a área de transferência para partilha",
"LABEL_VIDEO_DEBUG": "Info de Debug de Vídeo",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Detalhes técnicos sobre o elemento de vídeo atualmente seleccionado",
"VIDEO_DEBUG_EMPTY": "Nenhum separador selecionado ou vídeo detetado.",
"LABEL_HISTORY": "Histórico Completo de Ações",
"LABEL_HISTORY_TOOLTIP": "Registo cronológico de todos os comandos de sincronização na sala",
"HISTORY_EMPTY": "Nenhuma atividade ainda",
"LABEL_LOGS": "Registos (Últimos 50)",
"LABEL_LOGS_TOOLTIP": "Registos de ligação técnica para depuração",
"BTN_CLEAR": "LIMPAR",
"BTN_CLEAR_TOOLTIP": "Limpar a saída do registo",
"LABEL_GITHUB": "Repositório GitHub",
"BTN_ONBOARDING_SKIP": "Saltar",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Saltar o tutorial",
"BTN_ONBOARDING_NEXT": "Seguinte",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Ir para o passo seguinte",
"ONBOARDING_1_TITLE": "Bem-vindo ao KoalaSync!",
"ONBOARDING_1_TEXT": "Veja vídeos em conjunto numa sincronizzazione perfetta — não importa onde esteja. Vamos fare uma breve visita guiada!",
"ONBOARDING_2_TITLE": "1. Criar uma Sala",
"ONBOARDING_2_TEXT": "Comece aqui. Crie uma sala e partilhe o link de convite com os seus amigos.",
"ONBOARDING_3_TITLE": "2. Selecionar Vídeo",
"ONBOARDING_3_TEXT": "Navegue até aqui para selecionar o vídeo que deseja sincronizar. Reproduza, pause e avance — todos ficam em sincronia.",
"ONBOARDING_4_TITLE": "3. Personalizar",
"ONBOARDING_4_TEXT": "Escolha um nome de utilizador divertido para que os seus amigos saibam quem você é.",
"ONBOARDING_5_TITLE": "Está tudo pronto!",
"ONBOARDING_5_TEXT": "Altura de ir buscar pipocas. Divirtam-se a ver juntos!",
"ERR_CONN_TIMEOUT": "Ligação expirada. Tente novamente.",
"ERR_INVALID_SERVER_URL": "Formato de URL do Servidor inválido.",
"ERR_IDENTITY_NOT_LOADED": "Identidade ainda não carregada. Aguarde um momento e tente novamente.",
"ERR_NO_PEERS_TIME": "Nenhum outro peer com hora conhecida. Mude para 'Ir para Mim'.",
"ERR_NO_VIDEO_TAB": "Não foi possível ligar ao separador do vídeo.",
"ERR_SELECT_VIDEO": "Selecione primeiro um vídeo!",
"TOAST_INVITE_COPIED": "Link de convite copiado!",
"TOAST_COPY_FAILED": "Falha ao copiar para a área de transferência",
"TOAST_LOBBY_SKIPPED": "Lobby do episódio ignorado.",
"TOAST_LOBBY_SKIP_FAILED": "Falha ao ignorar o lobby.",
"TOAST_LOGS_COPIED": "Copiado!",
"TOAST_PEER_JOINED": "{name} entrou na sala",
"TOAST_PEER_LEFT": "{name} saiu da sala",
"TOAST_PEER_ACTION": "{name} {action}",
"STATUS_CONNECTED": "Ligado",
"STATUS_RECONNECTING": "A voltar a ligar...",
"STATUS_CONNECTING": "A ligar...",
"STATUS_FAILED": "Falhou",
"STATUS_DISCONNECTED": "Desligado",
"BTN_STATE_JOINING": "🚀 A entrar...",
"BTN_STATE_RECONNECTING": "🔄 A voltar a ligar...",
"BTN_STATE_PLAYING": "▶ A reproduzir...",
"BTN_STATE_PAUSING": "⏸ A pausar...",
"BTN_STATE_SYNCING_GROUP": "A sincronizar com o grupo ({time})....",
"BTN_STATE_SYNCING": "A sincronizar...",
"BTN_STATE_SYNCED": "✅ Sincronizado!",
"NOTIF_PLAY": "iniciou a reprodução",
"NOTIF_PAUSE": "pausou a reprodução",
"NOTIF_SEEK": "avançou/recuou no vídeo",
"NOTIF_FORCE_PREPARE": "iniciou a sincronização forçada",
"NOTIF_FORCE_EXECUTE": "sincronizou todos",
"DEBUG_NO_TAB": "Nenhum separador de destino selecionado.",
"DEBUG_COMM_FAIL": "Não foi possível comunicar com o vídeo do separador.",
"EMPTY_PEERS_TITLE": "Nenhum participante ainda",
"EMPTY_PEERS_HINT": "Partilhe o seu link de convite para começar",
"EMPTY_HISTORY_TITLE": "Nenhuma atividade ainda",
"EMPTY_HISTORY_HINT": "Reproduza, pause ou avance para ver o histórico",
"EMPTY_LOGS_TITLE": "Nenhum registo",
"EMPTY_LOGS_HINT": "Os eventi de ligação vão aparecer aqui",
"EMPTY_ROOMS_TITLE": "Nenhuma sala ativa",
"EMPTY_ROOMS_HINT": "Crie uma sala ou atualize para encontrar salas públicas",
"LABEL_YOU": "Tu",
"ONBOARDING_DONE": "Concluído!",
"LABEL_LOBBY_PEER_READY": "Pronto",
"LABEL_LOBBY_PEER_LOADING": "A carregar...",
"LABEL_PASSWORD_PROTECTED": "Protegido por Palavra-passe",
"LABEL_PEERS_COUNT": "{count} participantes",
"LABEL_CUSTOM_SERVER": "Servidor Personalizado",
"BTN_STATE_CREATING": "🚀 A Criar Sala...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Sincronização do Episódio Falhou",
"NOTIF_LOBBY_CANCEL_MSG": "Sincronização automática cancelada: {reason}. Pode ter de sincronizar manualmente.",
"LOBBY_CANCEL_TIMEOUT": "Timeout",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Timeout (recuperado)",
"LOBBY_CANCEL_PEERS_LEFT": "Todos os outros participantes saíram",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — nem todos os participantes carregaram o episódio",
"LOBBY_CANCEL_USER": "Cancelado pelo utilizador",
"NOTIF_ERROR_TITLE": "Erro do KoalaSync"
}
+3
View File
@@ -33,6 +33,9 @@
"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": "НЕТ",
+183
View File
@@ -0,0 +1,183 @@
{
"LANG_CODE": "tr",
"HTML_CLASS": "lang-tr",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Oda",
"TAB_ROOM_TOOLTIP": "Oda ayarları ve bağlantı",
"TAB_SYNC": "Senkronizasyon",
"TAB_SYNC_TOOLTIP": "Video senkronizasyon kontrolleri ve uzaktan işlemler",
"TAB_SETTINGS": "Ayarlar",
"TAB_SETTINGS_TOOLTIP": "Eklenti tercihleri",
"TAB_STATUS": "Durum",
"TAB_STATUS_TOOLTIP": "Gelişmiş Teşhis ve Günlükler",
"BTN_CREATE_ROOM": "+ Yeni Oda Oluştur",
"MANUAL_CONNECT_HEADER": "Manuel Bağlantı / Gelişmiş",
"LABEL_SERVER": "Sunucu",
"BTN_SERVER_OFFICIAL": "Resmi",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Resmi güvenilir sunucuyu kullanın",
"BTN_SERVER_CUSTOM": "Özel",
"BTN_SERVER_CUSTOM_TOOLTIP": "Kendi barındırdığınız sunucuya bağlanın",
"PLACEHOLDER_SERVER_URL": "wss://sunucunuz:3000",
"LABEL_ROOM_ID": "Oda Kimliği",
"LABEL_ROOM_ID_TOOLTIP": "Senkronizasyon odanız için benzersiz kimlik",
"PLACEHOLDER_ROOM_ID": "Oda Kimliğini Girin",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "Katılmak istediğiniz odanın benzersiz kimliği",
"LABEL_PASSWORD": "Şifre (İsteğe Bağlı)",
"LABEL_PASSWORD_TOOLTIP": "Oda erişimini kısıtlamak için isteğe bağlı şifre",
"PLACEHOLDER_PASSWORD": "Oda Şifresi (isteğe bağlı)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Oda şifresi (yoksa boş bırakın)",
"BTN_JOIN_ROOM": "Odaya Katıl / Oluştur",
"BTN_JOIN_ROOM_TOOLTIP": "Odaya bağlan",
"LABEL_PUBLIC_ROOMS": "Açık Odalar",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Bu sunucuda herkese açık odaların listesi",
"BTN_REFRESH": "YENİLE",
"BTN_REFRESH_TOOLTIP": "Genel odaların listesini yenile",
"PUBLIC_ROOMS_REFRESHING": "Yenileniyor...",
"BTN_REFRESH_COOLDOWN": "{seconds}sn BEKLE",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Oda listesi yenileme soğuyor. {seconds}sn sonra tekrar deneyin.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Açık odalar yenileniyor. Bir sonraki yenileme {seconds}sn sonra yapılabilecek.",
"LABEL_ACTIVE_ROOM": "Aktif Oda",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Şu anda bağlı olduğunuz oda",
"ACTIVE_ROOM_NONE": "YOK",
"ACTIVE_SERVER_OFFICIAL": "Resmi Sunucu",
"LABEL_INVITE_LINK": "Davet Linki",
"LABEL_INVITE_LINK_TOOLTIP": "Katılabilmeleri için bu bağlantıyı arkadaşlarınızla paylaşın",
"LABEL_PEERS_IN_ROOM": "Odadaki Kişiler",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Şu anda bu odaya bağlı olan diğer kullanıcılar",
"NO_PEERS_CONNECTED": "Bağlı kimse yok",
"BTN_LEAVE_ROOM": "Odadan Ayrıl",
"LABEL_SELECT_VIDEO": "Video Seç",
"LABEL_SELECT_VIDEO_TOOLTIP": "Senkronize edilecek videoyu içeren tarayıcı sekmesini seçin",
"OPTION_SELECT_TAB": "-- Bir Sekme Seçin --",
"LABEL_REMOTE_CONTROL": "Uzaktan Kumanda",
"BTN_COPY_INVITE": "📋 Davet Linki",
"BTN_COPY_INVITE_TOOLTIP": "Davet Linkini Kopyala",
"BTN_PLAY": "▶ Oynat",
"BTN_PLAY_TOOLTIP": "Herkese Oynat komutu gönder",
"BTN_PAUSE": "⏸ Duraklat",
"BTN_PAUSE_TOOLTIP": "Herkese Duraklat komutu gönder",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Tüm kullanıcıları senkronize etmeye zorla",
"OPTION_JUMP_TO_OTHERS": "Diğerlerine Atla",
"OPTION_JUMP_TO_ME": "Bana Atla",
"OPTION_JUMP_MODE_TOOLTIP": "Senkronizasyon hedefini seçin",
"LABEL_LAST_ACTIVITY": "Son Etkinlik Durumu",
"LABEL_LAST_ACTIVITY_TOOLTIP": "En son oynatma, duraklatma veya arama komutunu gösterir",
"NO_RECENT_COMMANDS": "Son komut yok",
"LOBBY_HEADER": "BÖLÜM LOBİSİ",
"LOBBY_WAITING_FOR": "🎬 Beklenen: \"{title}\"",
"LOBBY_WAITING_PEERS": "Bağlantılar bekleniyor...",
"BTN_SKIP_PLAY": "Atla ve Yine de Oynat",
"BTN_SKIP_PLAY_TOOLTIP": "Lobiyi iptal et ve yine de oynat",
"LOBBY_CONNECT_FIRST": "Önce bir odaya bağlanın",
"LOBBY_CONNECT_FIRST_DESC": "Videoları senkronize etmek için bir davet bağlantısı aracılığıyla bir odaya katılmanız veya yeni bir oda oluşturmanız gerekir.",
"BTN_CREATE_ROOM_ALT": "Yeni Oda Oluştur",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Yeni rastgele bir oda oluşturun ve katılın",
"LABEL_USERNAME": "Kullanıcı Adınız",
"LABEL_USERNAME_TOOLTIP": "Kullanıcı adı başkalarının sizi tanımasına yardımcı olur.",
"PLACEHOLDER_USERNAME": "Anonim Koala",
"LABEL_HIDE_CLUTTER": "Gereksiz Sekmeleri Gizle",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Listeyi temiz tutmak için video olmayan sekmeleri ve alakasız alan adlarını filtreler",
"LABEL_AUTO_SYNC_NEXT": "Sonraki Bölümü Otomatik Eşitle",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Bir bölüm değiştiğinde otomatik olarak duraklatılır ve tüm bağlantıların hazır olmasını bekler, ardından birlikte senkronize olarak başlar.",
"LABEL_AUTO_COPY_INVITE": "Davet Linkini Otomatik Kopyala",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Yeni bir oda oluşturulduğunda davet bağlantısını panoya otomatik olarak kopyalar.",
"LABEL_NOTIFICATIONS": "Tarayıcı Bildirimleri",
"LABEL_NOTIFICATIONS_TOOLTIP": "Biri katıldığında/ayrıldığında veya oynattığında/duraklattığında yerel sistem bildirimlerini gösterir.",
"LABEL_LANGUAGE": "Uygulama Dili",
"LABEL_LANGUAGE_TOOLTIP": "Tercih ettiğiniz eklenti dilini seçin",
"LABEL_TROUBLESHOOTING": "Sorun Giderme",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Bağlantı sorunlarını giderme araçları",
"BTN_REGEN_ID": "Bağlantı Kimliğini Yeniden Oluştur",
"BTN_REGEN_ID_TOOLTIP": "Dahili kimliğinizi yeniden oluşturun ve tekrar bağlanın",
"REGEN_ID_DESC": "\"Duplicate Identity\" (Mükerrer Kimlik) hataları görüyorsanız bunu kullanın.",
"LABEL_CONN_STATUS": "Bağlantı Durumu",
"LABEL_CONN_STATUS_TOOLTIP": "Mevcut WebSocket bağlantı durumu",
"CONN_STATUS_DISCONNECTED": "Bağlantı Kesildi",
"BTN_RETRY": "TEKRAR DENE",
"BTN_RETRY_TOOLTIP": "Sunucuya yeniden bağlanmayı dene",
"BTN_COPY_LOGS": "Günlükleri Kopyala",
"BTN_COPY_LOGS_TOOLTIP": "Paylaşmak için günlükleri panoya kopyalayın",
"LABEL_VIDEO_DEBUG": "Video Hata Ayıklama Bilgisi",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Şu anda seçili video öğesi hakkında teknik ayrıntılar",
"VIDEO_DEBUG_EMPTY": "Sekme seçilmedi veya video algılanmadı.",
"LABEL_HISTORY": "Tam İşlem Geçmişi",
"LABEL_HISTORY_TOOLTIP": "Odadaki tüm senkronizasyon komutlarının kronolojik günlüğü",
"HISTORY_EMPTY": "Henüz etkinlik yok",
"LABEL_LOGS": "Günlükler (Son 50)",
"LABEL_LOGS_TOOLTIP": "Hata ayıklama için teknik bağlantı günlükleri",
"BTN_CLEAR": "TEMİZLE",
"BTN_CLEAR_TOOLTIP": "Günlük çıktısını temizle",
"LABEL_GITHUB": "GitHub Deposu",
"BTN_ONBOARDING_SKIP": "Atla",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Eğitimi atla",
"BTN_ONBOARDING_NEXT": "İleri",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Bir sonraki adıma git",
"ONBOARDING_1_TITLE": "KoalaSync'e Hoş Geldiniz!",
"ONBOARDING_1_TEXT": "Nerede olursanız olun videoları mükemmel bir senkronizasyonla birlikte izleyin. Hızlı bir tura çıkalım!",
"ONBOARDING_2_TITLE": "1. Oda Oluştur",
"ONBOARDING_2_TEXT": "Buradan başlayın. Bir oda oluşturun ve davet bağlantısını arkadaşlarınızla paylaşın.",
"ONBOARDING_3_TITLE": "2. Video Seç",
"ONBOARDING_3_TEXT": "Senkronize etmek istediğiniz videoyu seçmek için buraya gidin. Oynatın, duraklatın ve arayın — herkes senkronize kalır.",
"ONBOARDING_4_TITLE": "3. Kişiselleştir",
"ONBOARDING_4_TEXT": "Arkadaşlarınızın kim olduğunuzu bilmesi için eğlenceli bir kullanıcı adı seçin.",
"ONBOARDING_5_TITLE": "Her şey hazır!",
"ONBOARDING_5_TEXT": "Patlamış mısır alma zamanı. Birlikte izlemenin keyfini çıkarın!",
"ERR_CONN_TIMEOUT": "Bağlantı zaman aşımına uğradı. Lütfen tekrar deneyin.",
"ERR_INVALID_SERVER_URL": "Geçersiz Sunucu URL formatı.",
"ERR_IDENTITY_NOT_LOADED": "Kimlik henüz yüklenmedi. Biraz bekleyin ve tekrar deneyin.",
"ERR_NO_PEERS_TIME": "Bilinen bir zamanı olan başka bağlantı yok. 'Bana Atla' seçeneğine geçin.",
"ERR_NO_VIDEO_TAB": "Video sekmesine bağlanılamadı.",
"ERR_SELECT_VIDEO": "Lütfen önce bir video seçin!",
"TOAST_INVITE_COPIED": "Davet bağlantısı kopyalandı!",
"TOAST_COPY_FAILED": "Panoya kopyalanamadı",
"TOAST_LOBBY_SKIPPED": "Bölüm Lobisi atlandı.",
"TOAST_LOBBY_SKIP_FAILED": "Lobi atlanamadı.",
"TOAST_LOGS_COPIED": "Kopyalandı!",
"TOAST_PEER_JOINED": "{name} odaya katıldı",
"TOAST_PEER_LEFT": "{name} odadan ayrıldı",
"TOAST_PEER_ACTION": "{name} {action}",
"STATUS_CONNECTED": "Bağlandı",
"STATUS_RECONNECTING": "Yeniden bağlanılıyor...",
"STATUS_CONNECTING": "Bağlanılıyor...",
"STATUS_FAILED": "Başarısız",
"STATUS_DISCONNECTED": "Bağlantı Kesildi",
"BTN_STATE_JOINING": "🚀 Katılınıyor...",
"BTN_STATE_RECONNECTING": "🔄 Yeniden bağlanılıyor...",
"BTN_STATE_PLAYING": "▶ Oynatılıyor...",
"BTN_STATE_PAUSING": "⏸ Duraklatılıyor...",
"BTN_STATE_SYNCING_GROUP": "Gruba eşitleniyor ({time})...",
"BTN_STATE_SYNCING": "Senkronize ediliyor...",
"BTN_STATE_SYNCED": "✅ Eşitlendi!",
"NOTIF_PLAY": "oynatmayı başlattı",
"NOTIF_PAUSE": "oynatmayı duraklattı",
"NOTIF_SEEK": "videoda arama yaptı",
"NOTIF_FORCE_PREPARE": "zorunlu eşitleme başlattı",
"NOTIF_FORCE_EXECUTE": "herkesi eşitledi",
"DEBUG_NO_TAB": "Hedef sekme seçilmedi.",
"DEBUG_COMM_FAIL": "Sekme videosuyla iletişim kurulamadı.",
"EMPTY_PEERS_TITLE": "Henüz kimse yok",
"EMPTY_PEERS_HINT": "Başlamak için davet bağlantınızı paylaşın",
"EMPTY_HISTORY_TITLE": "Henüz etkinlik yok",
"EMPTY_HISTORY_HINT": "Geçmişi görmek için oynatın, duraklatın veya arayın",
"EMPTY_LOGS_TITLE": "Günlük yok",
"EMPTY_LOGS_HINT": "Bağlantı olayları burada görünecektir",
"EMPTY_ROOMS_TITLE": "Aktif oda yok",
"EMPTY_ROOMS_HINT": "Bir oda oluşturun veya genel odaları bulmak için yenileyin",
"LABEL_YOU": "Siz",
"ONBOARDING_DONE": "Tamamlandı!",
"LABEL_LOBBY_PEER_READY": "Hazır",
"LABEL_LOBBY_PEER_LOADING": "Yükleniyor...",
"LABEL_PASSWORD_PROTECTED": "Şifre Korumalı",
"LABEL_PEERS_COUNT": "{count} kişi",
"LABEL_CUSTOM_SERVER": "Özel Sunucu",
"BTN_STATE_CREATING": "🚀 Oda Oluşturuluyor...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Bölüm Eşitlemesi Başarısız",
"NOTIF_LOBBY_CANCEL_MSG": "Otomatik eşitleme iptal edildi: {reason}. Manuel olarak eşitlemeniz gerekebilir.",
"LOBBY_CANCEL_TIMEOUT": "Zaman aşımı",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Zaman aşımı (kurtarıldı)",
"LOBBY_CANCEL_PEERS_LEFT": "Diğer tüm bağlantılar ayrıldı",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Zaman aşımı — tüm bağlantılar bölümü yüklemedi",
"LOBBY_CANCEL_USER": "Kullanıcı tarafından iptal edildi",
"NOTIF_ERROR_TITLE": "KoalaSync Hatası"
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "KoalaSync",
"version": "1.9.3",
"version": "2.1.1",
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
"permissions": [
"storage",
+41 -13
View File
@@ -4,6 +4,13 @@
<meta charset="utf-8">
<title>KoalaSync</title>
<style>
@font-face {
font-family: 'Twemoji Country Flags';
unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;
src: url('assets/TwemojiCountryFlags.woff2') format('woff2');
font-display: swap;
}
:root {
--bg: #0f172a;
--card: #1e293b;
@@ -23,7 +30,7 @@
padding: 16px;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-family: 'Twemoji Country Flags', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
}
@@ -127,7 +134,7 @@
font-weight: 700;
}
input, select {
input, select, option {
width: 100%;
box-sizing: border-box;
background: var(--card);
@@ -139,6 +146,20 @@
font-family: inherit;
}
@supports (appearance: base-select) {
#langSelector,
#langSelector::picker(select) {
appearance: base-select;
}
#langSelector::picker(select) {
background-color: var(--bg);
border: 1px solid #334155;
border-radius: 8px;
padding: 4px;
font-family: 'Twemoji Country Flags', inherit;
}
}
input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
@@ -375,9 +396,9 @@
<div class="info-card" style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; border-left: 4px solid var(--accent);">
<div>
<label style="margin-bottom: 0;" title="The room you are currently connected to" data-i18n="LABEL_ACTIVE_ROOM" data-i18n-title="LABEL_ACTIVE_ROOM_TOOLTIP">Active Room</label>
<div id="activeRoomId" style="font-weight: 700; color: var(--accent); font-size: 16px; letter-spacing: 1px;" data-i18n="ACTIVE_ROOM_NONE">NONE</div>
<div id="activeRoomId" style="font-weight: 700; color: var(--accent); font-size: 16px; letter-spacing: 1px;">NONE</div>
</div>
<div id="activeServer" style="font-size: 10px; color: var(--text-muted); text-align: right; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" data-i18n="ACTIVE_SERVER_OFFICIAL">Official Server</div>
<div id="activeServer" style="font-size: 10px; color: var(--text-muted); text-align: right; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">Official Server</div>
</div>
<div class="info-card" style="margin-bottom: 20px;">
@@ -497,13 +518,20 @@
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;" data-i18n="LABEL_LANGUAGE" data-i18n-title="LABEL_LANGUAGE_TOOLTIP" title="Choose your preferred extension language">App Language</label>
<select id="langSelector" style="width: 150px; background: var(--bg); border: 1px solid #334155; color: white; padding: 6px 10px; border-radius: 8px; font-size: 13px; cursor: pointer; outline: none; font-family: inherit;">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="ru">Русский</option>
<select id="langSelector" style="width: 165px; background: var(--bg); border: 1px solid #334155; color: white; padding: 6px 10px; border-radius: 8px; font-size: 13px; cursor: pointer; outline: none; font-family: inherit;">
<option value="en">🇬🇧 English</option>
<option value="de">🇩🇪 Deutsch</option>
<option value="fr">🇫🇷 Français</option>
<option value="es">🇪🇸 Español</option>
<option value="pt-BR">🇧🇷 Português (Brasil)</option>
<option value="ru">🇷🇺 Русский</option>
<option value="it">🇮🇹 Italiano</option>
<option value="pl">🇵🇱 Polski</option>
<option value="tr">🇹🇷 Türkçe</option>
<option value="nl">🇳🇱 Nederlands</option>
<option value="ja">🇯🇵 日本語</option>
<option value="ko">🇰🇷 한국어</option>
<option value="pt">🇵🇹 Português (Portugal)</option>
</select>
</div>
@@ -519,13 +547,13 @@
<label title="Current WebSocket connection state" data-i18n="LABEL_CONN_STATUS" data-i18n-title="LABEL_CONN_STATUS_TOOLTIP">Connection Status</label>
<div id="connStatus" class="info-card" style="display:flex; align-items:center; gap: 10px;">
<span id="connDot" class="status-dot status-offline"></span>
<span id="connText" style="flex:1;" data-i18n="CONN_STATUS_DISCONNECTED">Disconnected</span>
<span id="connText" style="flex:1;">Disconnected</span>
<button id="retryBtn" class="secondary" style="display:none; width: auto; padding: 4px 8px; font-size: 10px; margin: 0;" title="Attempt to reconnect to the server" data-i18n="BTN_RETRY" data-i18n-title="BTN_RETRY_TOOLTIP">RETRY</button>
<button id="copyLogs" class="btn secondary" style="width: auto; padding: 4px 10px; font-size: 11px;" title="Copy logs to clipboard for sharing" data-i18n="BTN_COPY_LOGS" data-i18n-title="BTN_COPY_LOGS_TOOLTIP">Copy Logs</button>
</div>
<label title="Technical details about the currently selected video element" data-i18n="LABEL_VIDEO_DEBUG" data-i18n-title="LABEL_VIDEO_DEBUG_TOOLTIP">Video Debug Info</label>
<div id="videoDebug" class="info-card" style="font-size: 10px; font-family: monospace; color: var(--text-muted); max-height: 250px; overflow-y: auto; line-height: 1.4;" data-i18n="VIDEO_DEBUG_EMPTY">
<div id="videoDebug" class="info-card" style="font-size: 10px; font-family: monospace; color: var(--text-muted); max-height: 250px; overflow-y: auto; line-height: 1.4;">
No tab selected or video detected.
</div>
+163 -13
View File
@@ -1,7 +1,7 @@
import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js';
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
import { getAvatarForName, generateUsername, USERNAME_ADJECTIVES, USERNAME_NOUNS } from './shared/names.js';
import { loadLocale, translateDOM, getMessage } from './i18n.js';
import { loadLocale, translateDOM, getMessage, getSystemLanguage } from './i18n.js';
const elements = {
@@ -66,21 +66,57 @@ let popupIntervals = [];
let populateTabsToken = null;
let errorToken = 0;
let forceSyncDone = false;
let connectionErrorTimer = null;
let pendingConnectionErrorMsg = null;
let roomListRefreshTimer = null;
let roomListRefreshInterval = null;
const ROOM_LIST_REFRESH_COOLDOWN_MS = 11000;
// --- Helpers ---
function clearConnectionErrorTimer() {
if (connectionErrorTimer) {
clearTimeout(connectionErrorTimer);
connectionErrorTimer = null;
}
pendingConnectionErrorMsg = null;
}
function setRoomRefreshCooldown() {
if (roomListRefreshTimer) clearTimeout(roomListRefreshTimer);
if (roomListRefreshInterval) clearInterval(roomListRefreshInterval);
const originalLabel = getMessage('BTN_REFRESH');
const updateLabel = () => {
const secondsLeft = Math.max(1, Math.ceil((cooldownEndsAt - Date.now()) / 1000));
elements.refreshRooms.textContent = getMessage('BTN_REFRESH_COOLDOWN', { seconds: secondsLeft });
elements.refreshRooms.title = getMessage('BTN_REFRESH_COOLDOWN_TOOLTIP', { seconds: secondsLeft });
};
const cooldownEndsAt = Date.now() + ROOM_LIST_REFRESH_COOLDOWN_MS;
elements.refreshRooms.disabled = true;
updateLabel();
roomListRefreshInterval = setInterval(updateLabel, 250);
roomListRefreshTimer = setTimeout(() => {
elements.refreshRooms.disabled = false;
elements.refreshRooms.textContent = originalLabel;
elements.refreshRooms.title = getMessage('BTN_REFRESH_TOOLTIP');
clearInterval(roomListRefreshInterval);
roomListRefreshInterval = null;
roomListRefreshTimer = null;
}, ROOM_LIST_REFRESH_COOLDOWN_MS);
}
// --- Initialization ---
async function init() {
// Load Settings
const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'filterNoise', 'username', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale']);
let activeLang = data.locale;
if (!activeLang) {
const systemLang = (navigator.language || chrome.i18n.getUILanguage()).split('-')[0];
activeLang = ['en', 'de', 'fr', 'es', 'pt', 'ru'].includes(systemLang) ? (systemLang === 'pt' ? 'pt-BR' : systemLang) : 'en';
activeLang = getSystemLanguage();
chrome.storage.sync.set({ locale: activeLang });
}
await loadLocale(activeLang);
translateDOM();
@@ -120,6 +156,9 @@ async function init() {
refreshLogs();
refreshHistory();
// Default connection status (localized) before async check
applyConnectionStatus('disconnected');
// Initial Status Check
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, async (res) => {
if (chrome.runtime.lastError) {
@@ -148,8 +187,8 @@ async function init() {
// Check for invite link on landing page
checkInviteLink();
// Initial room list fetch
chrome.runtime.sendMessage({ type: 'GET_ROOM_LIST' });
// Initialize public rooms placeholder
renderEmpty(elements.publicRooms, 'rooms');
// Debug Info Refresh
popupIntervals.push(setInterval(refreshDebugInfo, 2000));
@@ -813,7 +852,7 @@ function updateRoomList(rooms) {
function checkInviteLink() {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs[0];
if (tab && tab.url && tab.url.includes(OFFICIAL_LANDING_PAGE_URL) && tab.url.includes('#join:')) {
if (tab && tab.url && tab.url.startsWith(OFFICIAL_LANDING_PAGE_URL + '/') && tab.url.includes('#join:')) {
try {
const rawHash = tab.url.split('#join:')[1];
if (!rawHash) return;
@@ -910,9 +949,33 @@ if (elements.langSelector) {
await chrome.storage.sync.set({ locale: selectedLang });
await loadLocale(selectedLang);
translateDOM();
// Re-apply connection and room UI state since translateDOM may overwrite dynamic elements
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, async (res) => {
if (chrome.runtime.lastError) return;
if (res) {
localPeerId = res.peerId;
reconnectSlowMode = res.reconnectSlowMode || false;
applyConnectionStatus(res.status);
updatePeerList(res.peers);
lastKnownPeers = res.peers || [];
if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers);
const data = await chrome.storage.sync.get(['roomId', 'password', 'useCustomServer', 'serverUrl']);
updateUI(data.roomId, data.password, data.useCustomServer, data.serverUrl);
await populateTabs(res.peers, res.targetTabId);
if (res.episodeLobby) updateLobbyUI(res.episodeLobby, res.peers);
} else {
applyConnectionStatus('disconnected');
const data = await chrome.storage.sync.get(['roomId', 'password', 'useCustomServer', 'serverUrl']);
updateUI(data.roomId, data.password, data.useCustomServer, data.serverUrl);
await populateTabs();
}
});
refreshLogs();
refreshHistory();
populateTabs();
});
}
@@ -952,7 +1015,12 @@ function showToast(message, type = 'info', duration = 3000) {
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
toast.textContent = message || '';
toast.style.whiteSpace = 'pre-wrap';
const delay = Math.max(0, duration - 600);
toast.style.animation = `toastSlideIn 0.3s ease-out, toastFadeOut 0.3s ease-in ${delay}ms forwards`;
container.appendChild(toast);
setTimeout(() => toast.remove(), duration);
}
@@ -981,6 +1049,7 @@ elements.roomId.addEventListener('input', () => {
});
elements.joinBtn.addEventListener('click', async () => {
clearConnectionErrorTimer();
if (elements.joinBtn.disabled) return;
const roomIdInput = elements.roomId.value.trim();
const isCreating = !roomIdInput;
@@ -1040,6 +1109,7 @@ elements.joinBtn.addEventListener('click', async () => {
});
elements.leaveBtn.addEventListener('click', async () => {
clearConnectionErrorTimer();
chrome.runtime.sendMessage({ type: 'LEAVE_ROOM' });
await chrome.storage.sync.set({ roomId: '', password: '' });
elements.roomId.value = '';
@@ -1080,10 +1150,13 @@ if (syncTabCreateRoomBtn) syncTabCreateRoomBtn.addEventListener('click', () => {
});
elements.refreshRooms.addEventListener('click', () => {
if (elements.refreshRooms.disabled) return;
setRoomRefreshCooldown();
elements.publicRooms.replaceChildren();
const el = document.createElement('div');
el.style.cssText = 'text-align:center; color: var(--text-muted); font-size: 11px; padding: 10px;';
el.textContent = getMessage('PUBLIC_ROOMS_REFRESHING');
el.textContent = getMessage('PUBLIC_ROOMS_REFRESHING_COOLDOWN', { seconds: Math.ceil(ROOM_LIST_REFRESH_COOLDOWN_MS / 1000) });
elements.publicRooms.appendChild(el);
chrome.runtime.sendMessage({ type: 'GET_ROOM_LIST' });
});
@@ -1307,10 +1380,29 @@ async function refreshLogs() {
}
chrome.runtime.onMessage.addListener((msg) => {
if (!msg) return;
if (msg.type === 'LOG_UPDATE') {
refreshLogs();
if (msg.log && msg.log.type === 'error') {
showError(msg.log.message);
const errMsg = msg.log.message || '';
const isConnErr = typeof errMsg === 'string' && (
errMsg.toLowerCase().includes('connection') ||
errMsg.toLowerCase().includes('websocket')
);
if (isConnErr) {
pendingConnectionErrorMsg = msg.log.message;
if (!connectionErrorTimer) {
connectionErrorTimer = setTimeout(() => {
if (pendingConnectionErrorMsg) {
showError(pendingConnectionErrorMsg);
}
connectionErrorTimer = null;
pendingConnectionErrorMsg = null;
}, 5000);
}
} else {
showError(msg.log.message);
}
}
} else if (msg.type === 'ACTION_UPDATE') {
const state = msg.state;
@@ -1366,6 +1458,9 @@ chrome.runtime.onMessage.addListener((msg) => {
updatePeerList(msg.peers);
if (msg.peers) detectPeerChanges(msg.peers);
} else if (msg.type === 'CONNECTION_STATUS') {
if (msg.status === 'connected') {
clearConnectionErrorTimer();
}
if (msg.status !== 'reconnecting') {
applyConnectionStatus(msg.status);
reconnectSlowMode = false;
@@ -1397,6 +1492,13 @@ chrome.runtime.onMessage.addListener((msg) => {
} else if (msg.type === 'ROOM_LIST') {
updateRoomList(msg.rooms);
} else if (msg.type === 'JOIN_STATUS') {
if (joinBtnTimeout) {
clearTimeout(joinBtnTimeout);
joinBtnTimeout = null;
}
elements.joinBtn.disabled = false;
elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM');
if (msg.success) {
// Final confirmation of join from background
chrome.storage.sync.get(['roomId', 'password', 'useCustomServer', 'serverUrl'], (data) => {
@@ -1462,6 +1564,24 @@ elements.copyLogs.addEventListener('click', () => {
if (vs.platform) lines.push(`- **Platform:** ${safe(vs.platform, '?')}`);
lines.push(`- **Video Count:** ${safe(vs.videoCount, 0)} | **Shadow DOM:** ${vs.inShadowDom ? 'YES' : 'NO'}`);
lines.push('');
// Multi-video overview
const videos = Array.isArray(vs.allVideos) ? vs.allVideos : [];
if (videos.length > 1) {
lines.push('### All Videos on Page');
lines.push('');
lines.push('| # | Resolution | Muted | Paused | Ready | Duration | Selected |');
lines.push('|---|------------|-------|--------|-------|----------|----------|');
const rl = ['HAVE_NOTHING', 'HAVE_METADATA', 'HAVE_CURRENT_DATA', 'HAVE_FUTURE_DATA', 'HAVE_ENOUGH_DATA'];
for (const v of videos) {
if (!v) continue;
const sel = v.selected ? ' **\u2190 TARGET**' : '';
const dim = `${safe(v.width, '?')}x${safe(v.height, '?')}`;
const rs = (v.readyState != null && v.readyState >= 0 && v.readyState <= 4) ? rl[v.readyState] : '?';
lines.push(`| ${safe(v.index, '?')} | ${dim} | ${safe(v.muted, '?')} | ${safe(v.paused, '?')} | ${rs} | ${safe(v.duration, 0)}s |${sel} |`);
}
lines.push('');
}
}
// ── Connection ──
@@ -1541,7 +1661,7 @@ elements.copyLogs.addEventListener('click', () => {
for (const h of recent) {
if (!h) continue;
const ts = safe(h.timestamp, '');
const evt = safe(h.event, safe(h.type, '?'));
const evt = safe(h.action, '?');
const from = h.senderId ? ` (${h.senderId})` : (h.peerId ? ` (${h.peerId})` : '');
const extra = h.detail ? ` \u2192 ${h.detail}` : '';
lines.push(`[${ts}] ${evt}${from}${extra}`);
@@ -1571,6 +1691,28 @@ elements.copyLogs.addEventListener('click', () => {
const original = elements.copyLogs.textContent;
elements.copyLogs.textContent = getMessage('TOAST_LOGS_COPIED');
setTimeout(() => { elements.copyLogs.textContent = original; }, 2000);
// Construct rich multiline toast summary
const verStr = safe(status.version, '?');
const logsCount = logs.length;
const historyCount = history.length;
const peersCount = Array.isArray(status.peers) ? status.peers.length : 0;
let summary = `📋 Debug Report Copied!\n`;
summary += `• Version: v${verStr}\n`;
summary += `• System Logs: ${logsCount} entries\n`;
summary += `• Sync Actions: ${historyCount} captured\n`;
if (status.roomId) {
summary += `• Peers in Room: ${peersCount}\n`;
}
if (rawVideo && vs.found) {
const stateStr = vs.paused === true ? 'Paused' : (vs.paused === false ? 'Playing' : 'Unknown');
const timeStr = typeof vs.currentTime === 'number' ? `${Math.round(vs.currentTime)}s` : '?';
summary += `• Video: ${stateStr} at ${timeStr}\n`;
}
summary += `Paste it in markdown to view!`;
showToast(summary, 'success', 5000);
}).catch(() => {
showToast(getMessage('TOAST_COPY_FAILED'), 'error');
});
@@ -1735,6 +1877,14 @@ window.addEventListener('unload', () => {
clearTimeout(forceSyncResetTimer);
forceSyncResetTimer = null;
}
if (roomListRefreshTimer) {
clearTimeout(roomListRefreshTimer);
roomListRefreshTimer = null;
}
if (roomListRefreshInterval) {
clearInterval(roomListRefreshInterval);
roomListRefreshInterval = null;
}
});
// --- Episode Lobby UI ---
+79
View File
@@ -0,0 +1,79 @@
# Prometheus Community JSON Exporter Configuration Example
# File: 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"
- name: koalasync_rate_limit_connections
path: '{.rateLimitEntries.connections}'
help: "Number of entries tracked in the connection rate limiter"
- name: koalasync_rate_limit_events
path: '{.rateLimitEntries.events}'
help: "Number of entries in the event rate limiter"
- name: koalasync_rate_limit_health
path: '{.rateLimitEntries.health}'
help: "Number of entries in the health rate limiter"
- name: koalasync_rate_limit_admin_metrics_auth
path: '{.rateLimitEntries.adminMetricsAuth}'
help: "Number of entries in the admin metrics auth rate limiter"
- name: koalasync_rate_limit_auth_failures
path: '{.rateLimitEntries.authFailures}'
help: "Number of entries in the authentication failures cache"
- name: koalasync_rate_limit_room_list
path: '{.rateLimitEntries.roomList}'
help: "Number of entries in the room list cooldown cache"
+2 -5
View File
@@ -1,12 +1,12 @@
{
"name": "koalasync",
"version": "1.9.3",
"version": "2.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "koalasync",
"version": "1.9.3",
"version": "2.0.5",
"devDependencies": {
"archiver": "^7.0.1",
"esbuild": "^0.28.0",
@@ -1240,7 +1240,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1374,7 +1373,6 @@
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"bare-abort-controller": "*"
},
@@ -1897,7 +1895,6 @@
"integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
+3 -2
View File
@@ -1,12 +1,13 @@
{
"name": "koalasync",
"version": "1.9.3",
"version": "2.1.1",
"description": "KoalaSync Build Scripts",
"private": true,
"scripts": {
"build:extension": "node scripts/build-extension.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"verify": "node scripts/verify-release.mjs"
},
"devDependencies": {
"archiver": "^7.0.1",
+64
View File
@@ -0,0 +1,64 @@
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const contentPath = path.join(__dirname, '..', 'extension', 'content.js');
const source = fs.readFileSync(contentPath, 'utf8');
function extractFunction(name, text) {
const start = text.indexOf(`function ${name}`);
assert.notStrictEqual(start, -1, `${name} not found`);
const bodyStart = text.indexOf('{', start);
let depth = 0;
for (let i = bodyStart; i < text.length; i++) {
if (text[i] === '{') depth++;
if (text[i] === '}') depth--;
if (depth === 0) return text.slice(start, i + 1);
}
throw new Error(`${name} body did not terminate`);
}
function makeVideo(name, width, height, options = {}) {
return {
name,
tagName: 'VIDEO',
videoWidth: width,
videoHeight: height,
offsetWidth: width,
offsetHeight: height,
muted: options.muted ?? true,
duration: options.duration ?? 0
};
}
const lightPreview = makeVideo('light-preview', 160, 90, { muted: false, duration: 30 });
const shadowPlayer = makeVideo('shadow-player', 1920, 1080, { muted: false, duration: 3600 });
const shadowRoot = {
querySelectorAll(selector) {
if (selector === 'video') return [shadowPlayer];
return [];
}
};
const shadowHost = { shadowRoot };
const fakeDocument = {
querySelectorAll(selector) {
if (selector === 'video') return [lightPreview];
return [shadowHost];
}
};
const fnSource = extractFunction('findVideo', source);
const findVideo = Function('document', `${fnSource}; return findVideo;`)(fakeDocument);
const selected = findVideo(fakeDocument);
assert.strictEqual(
selected,
shadowPlayer,
'findVideo should score Shadow DOM videos together with light DOM videos'
);
console.log('content video finder tests passed');
+36 -2
View File
@@ -9,13 +9,47 @@ if (!fs.existsSync(enPath)) {
process.exit(1);
}
let hasError = false;
// Verify SUPPORTED_LANGUAGES in extension/i18n.js matches JSON files
const i18nPath = path.join(__dirname, '..', 'extension', 'i18n.js');
try {
if (fs.existsSync(i18nPath)) {
const i18nContent = fs.readFileSync(i18nPath, 'utf8');
const langMatch = i18nContent.match(/export const SUPPORTED_LANGUAGES = \[(.*?)\];/);
if (!langMatch) {
hasError = true;
console.error('❌ Could not parse SUPPORTED_LANGUAGES from extension/i18n.js');
} else {
const supportedLangs = langMatch[1].split(',').map(s => s.trim().replace(/['"]/g, ''));
const fileLangs = fs.readdirSync(localesDir)
.filter(file => file.endsWith('.json'))
.map(file => file.replace('.json', ''));
for (const lang of fileLangs) {
if (!supportedLangs.includes(lang)) {
hasError = true;
console.error(`${lang}.json exists in extension/locales but is missing from SUPPORTED_LANGUAGES in extension/i18n.js`);
}
}
for (const lang of supportedLangs) {
if (!fileLangs.includes(lang)) {
hasError = true;
console.error(`${lang} is in SUPPORTED_LANGUAGES in extension/i18n.js but ${lang}.json is missing from extension/locales`);
}
}
}
}
} catch (err) {
hasError = true;
console.error('❌ Failed to verify SUPPORTED_LANGUAGES synchronization:', err.message);
}
const enDict = JSON.parse(fs.readFileSync(enPath, 'utf8'));
const enKeys = Object.keys(enDict);
const localeFiles = fs.readdirSync(localesDir).filter(file => file.endsWith('.json') && file !== 'en.json');
let hasError = false;
console.log(`Auditing i18n locales using ${enKeys.length} baseline keys from en.json...\n`);
for (const file of localeFiles) {
+27
View File
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { cwd } from 'node:process';
const popupPath = path.join(cwd(), 'extension', 'popup.js');
const source = fs.readFileSync(popupPath, 'utf8');
assert.match(
source,
/const ROOM_LIST_REFRESH_COOLDOWN_MS\s*=\s*11000;/,
'popup should define an 11 second room-list refresh cooldown'
);
assert.match(
source,
/elements\.refreshRooms\.disabled\s*=\s*true;/,
'refresh button should be disabled while cooldown is active'
);
assert.match(
source,
/setTimeout\(\(\)\s*=>\s*{\s*elements\.refreshRooms\.disabled\s*=\s*false;/s,
'refresh button should be re-enabled after the cooldown'
);
console.log('popup refresh cooldown tests passed');
+85
View File
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict';
import {
buildHealthPayload,
checkCooldown,
getCachedPayload,
isAdminMetricsAuthorized,
isAdminMetricsTokenStrong
} from '../server/ops.js';
const missingAuth = isAdminMetricsAuthorized(undefined, 'secret-token');
assert.equal(missingAuth, false, 'missing Authorization header must not authorize metrics');
const wrongAuth = isAdminMetricsAuthorized('Bearer wrong-token', 'secret-token');
assert.equal(wrongAuth, false, 'wrong bearer token must not authorize metrics');
const correctAuth = isAdminMetricsAuthorized('Bearer secret-token', 'secret-token');
assert.equal(correctAuth, true, 'correct bearer token should authorize metrics');
const disabledAuth = isAdminMetricsAuthorized('Bearer secret-token', '');
assert.equal(disabledAuth, false, 'empty admin token disables admin metrics');
assert.equal(isAdminMetricsTokenStrong(''), true, 'empty admin token is allowed because metrics stay disabled');
assert.equal(isAdminMetricsTokenStrong('short-token'), false, 'short admin token should be reported as weak');
assert.equal(
isAdminMetricsTokenStrong('a'.repeat(32)),
true,
'admin token with at least 32 characters should be considered strong'
);
const cooldowns = new Map();
assert.equal(checkCooldown(cooldowns, 'socket-1', 10_000, 100_000), true, 'first cooldown check passes');
assert.equal(checkCooldown(cooldowns, 'socket-1', 10_000, 105_000), false, 'second cooldown check inside window fails');
assert.equal(checkCooldown(cooldowns, 'socket-1', 10_000, 110_000), true, 'cooldown check after window passes');
const cache = new Map();
let buildCalls = 0;
const firstCached = getCachedPayload(cache, 'basic-health', 60_000, () => ({ value: ++buildCalls }), 1_000);
const secondCached = getCachedPayload(cache, 'basic-health', 60_000, () => ({ value: ++buildCalls }), 30_000);
const expiredCached = getCachedPayload(cache, 'basic-health', 60_000, () => ({ value: ++buildCalls }), 61_001);
assert.deepEqual(firstCached, { value: 1 }, 'cache should return the builder payload on first request');
assert.strictEqual(secondCached, firstCached, 'cache should reuse payloads inside the ttl');
assert.deepEqual(expiredCached, { value: 2 }, 'cache should rebuild payloads after ttl expiry');
const roomA = { peers: new Set(['a', 'b']), activeLobby: null };
const roomB = { peers: new Set(['c', 'd', 'e']), activeLobby: { expectedTitle: 'Episode 2' } };
const rooms = new Map([['room-a', roomA], ['room-b', roomB]]);
const basicHealth = buildHealthPayload({
rooms,
connections: 5,
includeMetrics: false,
now: 1234,
uptime: 99,
memoryUsage: () => ({ rss: 10, heapUsed: 5, heapTotal: 8 }),
rateLimitSizes: { connections: 1, events: 2, health: 3, adminMetricsAuth: 4, authFailures: 5, roomList: 6 }
});
assert.deepEqual(
Object.keys(basicHealth).sort(),
['connections', 'rooms', 'status', 'timestamp', 'uptime'].sort(),
'basic health should not expose extended metrics'
);
const adminHealth = buildHealthPayload({
rooms,
connections: 5,
includeMetrics: true,
now: 1234,
uptime: 99,
memoryUsage: () => ({ rss: 10, heapUsed: 5, heapTotal: 8 }),
rateLimitSizes: { connections: 1, events: 2, health: 3, adminMetricsAuth: 4, authFailures: 5, roomList: 6 }
});
assert.equal(adminHealth.peers, 5, 'admin metrics should include aggregate peer count');
assert.equal(adminHealth.roomsWithLobby, 1, 'admin metrics should count active lobbies');
assert.equal(adminHealth.avgPeersPerRoom, 2.5, 'admin metrics should include average room size');
assert.equal(adminHealth.maxPeersInRoom, 3, 'admin metrics should include max room size');
assert.deepEqual(adminHealth.memory, { rss: 10, heapUsed: 5, heapTotal: 8 }, 'admin metrics should expose process memory');
assert.deepEqual(
adminHealth.rateLimitEntries,
{ connections: 1, events: 2, health: 3, adminMetricsAuth: 4, authFailures: 5, roomList: 6 },
'admin metrics should expose aggregate rate-limit map sizes'
);
console.log('server ops tests passed');
+94
View File
@@ -0,0 +1,94 @@
import assert from 'node:assert/strict';
import {
ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE,
HEALTH_RATE_LIMIT_PER_MINUTE,
healthCounts,
adminMetricsAuthCounts,
healthResponseCache,
httpServer,
rooms,
startServer,
stopServerForTests
} from '../server/index.js';
const adminToken = process.env.ADMIN_METRICS_TOKEN || 'test-admin-token-with-more-than-32-chars';
const baseHeaders = { 'x-forwarded-for': '203.0.113.10' };
function url(path) {
const address = httpServer.address();
return `http://127.0.0.1:${address.port}${path}`;
}
async function request(path, options = {}) {
return fetch(url(path), {
...options,
headers: {
...baseHeaders,
...(options.headers || {})
}
});
}
try {
await startServer(0, '127.0.0.1');
let res = await request('/');
assert.equal(res.status, 200, 'root health endpoint should respond');
assert.equal(res.headers.get('cache-control'), 'no-store', 'root response should disable HTTP caching');
assert.deepEqual(await res.json(), { status: 'online', service: 'KoalaSync Relay' });
res = await request('/health');
assert.equal(res.status, 200, 'basic health endpoint should respond');
assert.equal(res.headers.get('cache-control'), 'no-store', 'basic health response should disable HTTP caching');
const basicHealth = await res.json();
assert.equal(basicHealth.status, 'ok', 'basic health should report ok');
assert.equal(basicHealth.rooms, 0, 'basic health should include room count');
assert.equal('peers' in basicHealth, false, 'basic health should not expose admin metrics');
assert.equal('memory' in basicHealth, false, 'basic health should not expose memory metrics');
rooms.set('route-test-room', {
peers: new Set(['socket-a', 'socket-b']),
peerData: new Map(),
peerIds: new Map(),
activeLobby: { expectedTitle: 'Episode 1', initiatorPeerId: 'peer-a', readyPeers: ['peer-a'] }
});
healthResponseCache.clear();
healthCounts.clear();
res = await request('/health', {
headers: { authorization: `Bearer ${adminToken}`, 'x-forwarded-for': '203.0.113.20' }
});
assert.equal(res.status, 200, 'authorized admin health endpoint should respond');
const adminHealth = await res.json();
assert.equal(adminHealth.rooms, 1, 'admin health should include room count');
assert.equal(adminHealth.peers, 2, 'admin health should include aggregate peer count');
assert.equal(adminHealth.roomsWithLobby, 1, 'admin health should include aggregate lobby count');
assert.equal(typeof adminHealth.memory?.rss, 'number', 'admin health should include aggregate memory metrics');
assert.equal('route-test-room' in adminHealth, false, 'admin health should not expose room identifiers');
healthCounts.clear();
adminMetricsAuthCounts.clear();
for (let i = 0; i < ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE; i++) {
res = await request('/health', {
headers: { authorization: 'Bearer wrong-token', 'x-forwarded-for': '203.0.113.30' }
});
assert.equal(res.status, 200, `wrong admin bearer attempt ${i + 1} should still return basic health`);
}
res = await request('/health', {
headers: { authorization: 'Bearer wrong-token', 'x-forwarded-for': '203.0.113.30' }
});
assert.equal(res.status, 429, 'wrong admin bearer attempts should be throttled after the limit');
healthCounts.clear();
for (let i = 0; i < HEALTH_RATE_LIMIT_PER_MINUTE; i++) {
const path = i % 2 === 0 ? '/' : '/health';
res = await request(path, { headers: { 'x-forwarded-for': '203.0.113.40' } });
assert.equal(res.status, 200, `shared health request ${i + 1} should be allowed`);
}
res = await request('/', { headers: { 'x-forwarded-for': '203.0.113.40' } });
assert.equal(res.status, 429, 'root and health should share the public health rate limit');
console.log('server route tests passed');
} finally {
await stopServerForTests();
}
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const checks = [
['server ops', 'node', ['scripts/test-server-ops.mjs']],
['server routes', 'node', ['scripts/test-server-routes.mjs'], {
env: { ADMIN_METRICS_TOKEN: 'verify-admin-token-with-more-than-32-chars' }
}],
['content video finder', 'node', ['scripts/test-content-video-finder.js']],
['popup refresh cooldown', 'node', ['scripts/test-popup-refresh-cooldown.mjs']],
['server syntax index', 'node', ['-c', 'server/index.js']],
['server syntax ops', 'node', ['-c', 'server/ops.js']],
['content syntax', 'node', ['-c', 'extension/content.js']],
['popup syntax', 'node', ['-c', 'extension/popup.js']],
['background syntax', 'node', ['-c', 'extension/background.js']],
['locale coverage', 'node', ['scripts/test-locales.js']],
['lint', 'npm', ['run', 'lint']],
['root production audit', 'npm', ['audit', '--omit=dev']],
['server production audit', 'npm', ['audit', '--omit=dev'], { cwd: path.join(repoRoot, 'server') }],
['extension build', 'npm', ['run', 'build:extension']],
['website build', 'node', ['website/build.js']]
];
function runCheck([label, command, args, options = {}]) {
return new Promise((resolve, reject) => {
console.log(`\n==> ${label}`);
const child = spawn(command, args, {
cwd: options.cwd || repoRoot,
env: { ...process.env, ...(options.env || {}) },
stdio: 'inherit',
shell: process.platform === 'win32'
});
child.on('error', reject);
child.on('exit', (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`${label} failed with exit code ${code}`));
});
});
}
for (const check of checks) {
await runCheck(check);
}
console.log('\nRelease verification passed');
+10 -4
View File
@@ -1,4 +1,10 @@
PORT=3000
MIN_VERSION=1.0.0
MAX_ROOMS=1000
MAX_PEERS_PER_ROOM=50
PORT="3000"
MIN_VERSION="1.0.0"
MAX_ROOMS="1000"
MAX_PEERS_PER_ROOM="25"
# Optional: enables aggregate-only admin metrics on /health with Authorization: Bearer <token>
# Use a long random token, 32+ characters recommended.
ADMIN_METRICS_TOKEN=""
# Optional: set to "1" to enable verbose connection, room-join/leave, and CORS logs in the console. Default is "0" (disabled).
DEBUG_LOGGING="0"
+34 -4
View File
@@ -12,12 +12,39 @@ A Node.js relay server for synchronized video playback.
### Environment
Copy `.env.example` to `.env` and configure your settings.
```bash
PORT=3000
MAX_ROOMS=1000
MAX_PEERS_PER_ROOM=50
MIN_VERSION=1.0.0
PORT="3000"
MAX_ROOMS="1000"
MAX_PEERS_PER_ROOM="25"
MIN_VERSION="1.0.0"
# Optional: enables aggregate-only admin metrics on /health with Authorization: Bearer <token>
# Use a long random token, 32+ characters recommended.
ADMIN_METRICS_TOKEN=""
# Optional: set to "1" to enable verbose connection, room-join/leave, and CORS logs in the console. Default is "0" (disabled).
DEBUG_LOGGING="0"
```
### Health & Metrics
`GET /` and `GET /health` are IP-rate-limited to 10 requests per minute per client IP. These health-style responses are cached server-side for 60 seconds and refreshed lazily on request. By default `/health` returns only basic service status, uptime, room count, connection count, and a timestamp.
If `ADMIN_METRICS_TOKEN` is set, requests with `Authorization: Bearer <token>` receive additional aggregate metrics such as total peers, average peers per room, max room size, active lobby count, rate-limit map sizes, and process memory usage. Wrong admin bearer attempts are separately limited to 5 requests per minute per client IP. The metrics response does not include room IDs, peer IDs, usernames, IP addresses, media titles, or other user-level data.
Use a long random `ADMIN_METRICS_TOKEN` of at least 32 characters. Shorter configured tokens still work, but the server logs a startup warning.
Generate a token with one of these commands:
```bash
openssl rand -base64 32
# or
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
```
For Docker Compose deployments, set the generated value in `server/.env`:
```bash
ADMIN_METRICS_TOKEN=replace-with-a-long-random-token
```
When polling `/health` from Prometheus, Uptime Kuma, cron, or a load balancer, keep the interval comfortably below the public limit of 10 requests per minute per client IP. A 30-60 second interval is recommended for routine monitoring. Use the admin bearer token only from trusted monitoring hosts, and keep the Node server private behind Caddy or another trusted reverse proxy because IP-based limits depend on the configured proxy boundary.
### Docker (Recommended)
The server is available as a pre-built image on GHCR.
```bash
@@ -38,6 +65,9 @@ npm start
## Security
- **Rate Limiting**: IP-based connection limits and socket-based event limits.
- **Health Endpoint Throttle**: `GET /` and `GET /health` are limited to 10 requests per minute per IP, with 60-second lazy server-side response caching and stricter throttling for wrong admin bearer attempts.
- **Room Discovery Throttle**: Room-list refreshes are rate-limited server-side to one request every 10 seconds per socket.
- **Token Handshake**: Requires a valid token defined in the root `shared/constants.js`.
- **Single Source of Truth**: The server imports constants directly from the root `shared/` directory.
- **In-Memory**: Rooms are automatically pruned after 2 hours of inactivity.
- **Reverse Proxy Boundary**: The server trusts one reverse proxy hop for client IP detection. Keep the Node port private/firewalled so clients can only reach it through Caddy or another trusted proxy.
+166 -41
View File
@@ -1,9 +1,17 @@
import express from 'express';
import { createServer } from 'http';
import { fileURLToPath } from 'url';
import { Server } from 'socket.io';
import crypto from 'crypto';
import dotenv from 'dotenv';
import { EVENTS, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION } from '../shared/constants.js';
import {
buildHealthPayload,
checkCooldown,
getCachedPayload,
isAdminMetricsAuthorized,
isAdminMetricsTokenStrong
} from './ops.js';
dotenv.config();
@@ -15,19 +23,36 @@ function hashPassword(password) {
const PORT = process.env.PORT || 3000;
const MAX_ROOMS = parseInt(process.env.MAX_ROOMS) || 1000;
const MAX_PEERS_PER_ROOM = parseInt(process.env.MAX_PEERS_PER_ROOM) || 50;
const MAX_PEERS_PER_ROOM = parseInt(process.env.MAX_PEERS_PER_ROOM) || 25;
const MIN_VERSION = process.env.MIN_VERSION || '1.0.0';
const ADMIN_METRICS_TOKEN = process.env.ADMIN_METRICS_TOKEN || '';
const ROOM_LIST_COOLDOWN_MS = 10000;
export const HEALTH_RATE_LIMIT_PER_MINUTE = 10;
export const ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE = 5;
const HEALTH_RESPONSE_CACHE_TTL_MS = 60000;
const app = express();
if (!isAdminMetricsTokenStrong(ADMIN_METRICS_TOKEN)) {
console.warn('[SECURITY] ADMIN_METRICS_TOKEN is set but shorter than 32 characters. Use a long random token.');
}
export const app = express();
app.set('trust proxy', 1); // For real client IP through reverse proxy
export const healthResponseCache = new Map();
// Health Check with Rate Limiting
app.get('/', (req, res) => {
const clientIp = req.ip;
if (!checkHealthRate(clientIp)) {
return res.status(429).json({ error: 'Too many requests. Try again later.' });
}
res.json({ status: 'online', service: 'KoalaSync Relay' });
res.set('Cache-Control', 'no-store');
res.json(getCachedPayload(
healthResponseCache,
'root',
HEALTH_RESPONSE_CACHE_TTL_MS,
() => ({ status: 'online', service: 'KoalaSync Relay' })
));
});
app.get('/health', (req, res) => {
@@ -35,19 +60,37 @@ app.get('/health', (req, res) => {
if (!checkHealthRate(clientIp)) {
return res.status(429).json({ error: 'Rate limited' });
}
res.json({
status: 'ok',
uptime: process.uptime(),
rooms: rooms.size,
connections: io.engine?.clientsCount ?? 0,
timestamp: Date.now()
});
const authHeader = req.get('authorization');
const includeMetrics = isAdminMetricsAuthorized(authHeader, ADMIN_METRICS_TOKEN);
if (ADMIN_METRICS_TOKEN && authHeader && !includeMetrics && !checkAdminMetricsAuthRate(clientIp)) {
return res.status(429).json({ error: 'Rate limited' });
}
res.set('Cache-Control', 'no-store');
res.json(getCachedPayload(
healthResponseCache,
includeMetrics ? 'health-admin' : 'health-basic',
HEALTH_RESPONSE_CACHE_TTL_MS,
() => buildHealthPayload({
rooms,
connections: io.engine?.clientsCount ?? 0,
includeMetrics,
uptime: process.uptime(),
rateLimitSizes: {
connections: connectionCounts.size,
events: eventCounts.size,
health: healthCounts.size,
adminMetricsAuth: adminMetricsAuthCounts.size,
authFailures: failedAuthAttempts.size,
roomList: roomListCooldowns.size
}
})
));
});
const httpServer = createServer(app);
export const httpServer = createServer(app);
// Socket.IO setup with security constraints
const io = new Server(httpServer, {
export const io = new Server(httpServer, {
cors: {
origin: (origin, callback) => {
if (!origin || origin === 'https://sync.koalastuff.net' || origin.startsWith('chrome-extension://') || origin.startsWith('moz-extension://')) {
@@ -67,18 +110,22 @@ const io = new Server(httpServer, {
/**
* In-memory storage
*/
const rooms = new Map();
export const rooms = new Map();
const socketToRoom = new Map();
const peerToSocket = new Map(); // peerId -> socketId (Global lookup)
const roomCreationLocks = new Map(); // roomId -> Promise (prevents race on room creation)
function log(type, message, details = '') {
const debugLogging = process.env.DEBUG_LOGGING === '1';
const isVerbose = type === 'CONN' || type === 'ROOM' || type === 'DEDUPE' || type === 'CORS';
if (!debugLogging && isVerbose) return;
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${type}] ${message}`, details);
}
// Rate Limiting & Security
const connectionCounts = new Map(); // ip -> { count, resetTime }
export const connectionCounts = new Map(); // ip -> { count, resetTime }
const failedAuthAttempts = new Map(); // Map<IP+RoomID, {count, lastAttempt}>
function checkAuthRate(ip, roomId) {
@@ -106,17 +153,19 @@ function recordAuthFailure(ip, roomId) {
for (const [key, record] of failedAuthAttempts.entries()) {
if (now - record.lastAttempt > 15 * 60 * 1000) {
failedAuthAttempts.delete(key);
} else {
break; // Since entries are insertion-ordered by time, all subsequent entries are newer
}
}
// 2. If still over 50k, perform LRU-style eviction on the oldest 10,000 entries
// 2. If still over 50k, perform LRU-style eviction on the oldest 10,000 entries (first items in Map order)
if (failedAuthAttempts.size > 50000) {
log('SECURITY', 'failedAuthAttempts size exceeded 50000. Performing LRU-style eviction.');
const sortedEntries = Array.from(failedAuthAttempts.entries())
.sort((a, b) => a[1].lastAttempt - b[1].lastAttempt);
for (let i = 0; i < 10000 && i < sortedEntries.length; i++) {
failedAuthAttempts.delete(sortedEntries[i][0]);
log('SECURITY', 'failedAuthAttempts size exceeded 50000. Performing insertion-order eviction.');
for (const [key] of failedAuthAttempts.entries()) {
if (failedAuthAttempts.size <= 40000) {
break;
}
failedAuthAttempts.delete(key);
}
}
}
@@ -124,11 +173,12 @@ function recordAuthFailure(ip, roomId) {
const record = failedAuthAttempts.get(key) || { count: 0, lastAttempt: 0 };
record.count++;
record.lastAttempt = Date.now();
failedAuthAttempts.delete(key); // Remove first to update insertion order (moves key to the end of iteration)
failedAuthAttempts.set(key, record);
}
// Periodically clean up old auth failure records (every 15 minutes)
setInterval(() => {
const authFailureCleanupInterval = setInterval(() => {
const now = Date.now();
for (const [key, record] of failedAuthAttempts.entries()) {
if (now - record.lastAttempt > 15 * 60 * 1000) {
@@ -137,11 +187,13 @@ setInterval(() => {
}
}, 15 * 60 * 1000);
const eventCounts = new Map(); // socketId -> { count, resetTime }
const healthCounts = new Map(); // ip -> { count, resetTime }
export const eventCounts = new Map(); // socketId -> { count, resetTime }
export const healthCounts = new Map(); // ip -> { count, resetTime }
export const adminMetricsAuthCounts = new Map(); // ip -> { count, resetTime }
const roomListCooldowns = new Map(); // socketId -> last allowed timestamp
// Clean up connection counts and event counts to prevent memory leak
setInterval(() => {
const rateLimitCleanupInterval = setInterval(() => {
const now = Date.now();
for (const [ip, entry] of connectionCounts.entries()) {
if (now > entry.resetTime) {
@@ -158,6 +210,16 @@ setInterval(() => {
healthCounts.delete(ip);
}
}
for (const [ip, entry] of adminMetricsAuthCounts.entries()) {
if (now > entry.resetTime) {
adminMetricsAuthCounts.delete(ip);
}
}
for (const [socketId] of roomListCooldowns.entries()) {
if (!io.sockets.sockets.has(socketId)) {
roomListCooldowns.delete(socketId);
}
}
}, 60000);
function checkConnectionRate(ip) {
@@ -184,7 +246,16 @@ function checkHealthRate(ip) {
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
entry.count++;
healthCounts.set(ip, entry);
return entry.count <= 60;
return entry.count <= HEALTH_RATE_LIMIT_PER_MINUTE;
}
function checkAdminMetricsAuthRate(ip) {
const now = Date.now();
const entry = adminMetricsAuthCounts.get(ip) || { count: 0, resetTime: now + 60000 };
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
entry.count++;
adminMetricsAuthCounts.set(ip, entry);
return entry.count <= ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE;
}
/**
@@ -503,7 +574,7 @@ io.on('connection', (socket) => {
mediaTitle: clamp(data.mediaTitle, 100),
volume: clampNum(data.volume, 0, 1),
muted: validBool(data.muted),
peerId: typeof data.peerId === 'string' ? data.peerId.substring(0, 16) : undefined,
peerId: mapping.peerId,
status: typeof data.status === 'string' ? data.status.substring(0, 16) : undefined,
expectedTitle: clamp(data.expectedTitle, 100),
title: clamp(data.title, 100),
@@ -541,6 +612,10 @@ io.on('connection', (socket) => {
socket.disconnect(true);
return;
}
if (!checkCooldown(roomListCooldowns, socket.id, ROOM_LIST_COOLDOWN_MS)) {
socket.emit(EVENTS.ERROR, { message: 'Room list refresh is rate limited. Try again in a few seconds.' });
return;
}
const list = Array.from(rooms.entries()).map(([id, r]) => ({
id,
peerCount: r.peers.size,
@@ -584,6 +659,7 @@ io.on('connection', (socket) => {
socket.on('disconnect', () => {
eventCounts.delete(socket.id);
roomListCooldowns.delete(socket.id);
const mapping = socketToRoom.get(socket.id);
if (mapping) {
// Socket is already disconnected — no need to call socket.leave().
@@ -595,7 +671,7 @@ io.on('connection', (socket) => {
});
// Active Room & Dead Peer Cleanup (Every 2m)
setInterval(() => {
const roomCleanupInterval = setInterval(() => {
const now = Date.now();
const roomCutoff = now - (2 * 60 * 60 * 1000); // 2 hours
const peerCutoff = now - (5 * 60 * 1000); // 5 minutes
@@ -632,9 +708,29 @@ setInterval(() => {
}
}, 2 * 60 * 1000);
httpServer.listen(PORT, () => {
log('SERVER', `KoalaSync Relay running on port ${PORT}`);
});
export function startServer(port = PORT, host) {
if (httpServer.listening) return Promise.resolve(httpServer);
return new Promise((resolve, reject) => {
const onError = (err) => {
httpServer.off('listening', onListening);
reject(err);
};
const onListening = () => {
httpServer.off('error', onError);
const address = httpServer.address();
const actualPort = address && typeof address === 'object' ? address.port : port;
log('SERVER', `KoalaSync Relay running on port ${actualPort}`);
resolve(httpServer);
};
httpServer.once('error', onError);
httpServer.once('listening', onListening);
if (host) {
httpServer.listen(port, host);
} else {
httpServer.listen(port);
}
});
}
// --- M-4: Graceful Shutdown ---
function gracefulShutdown(signal) {
@@ -653,15 +749,44 @@ function gracefulShutdown(signal) {
}, 5000);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
export async function stopServerForTests() {
clearInterval(authFailureCleanupInterval);
clearInterval(rateLimitCleanupInterval);
clearInterval(roomCleanupInterval);
rooms.clear();
socketToRoom.clear();
peerToSocket.clear();
roomCreationLocks.clear();
connectionCounts.clear();
failedAuthAttempts.clear();
eventCounts.clear();
healthCounts.clear();
adminMetricsAuthCounts.clear();
roomListCooldowns.clear();
healthResponseCache.clear();
io.removeAllListeners();
io.disconnectSockets(true);
if (!httpServer.listening) return;
await new Promise((resolve, reject) => {
httpServer.close((err) => err ? reject(err) : resolve());
});
}
process.on('uncaughtException', (err) => {
log('ERROR', `Uncaught exception: ${err.message}`, err.stack);
process.exit(1);
});
const isMainModule = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
process.on('unhandledRejection', (reason) => {
log('ERROR', `Unhandled rejection: ${reason}`);
process.exit(1);
});
if (isMainModule) {
startServer(PORT);
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('uncaughtException', (err) => {
log('ERROR', `Uncaught exception: ${err.message}`, err.stack);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
log('ERROR', `Unhandled rejection: ${reason}`);
process.exit(1);
});
}
+92
View File
@@ -0,0 +1,92 @@
import crypto from 'crypto';
export function checkCooldown(cooldowns, key, windowMs, now = Date.now()) {
const lastAllowedAt = cooldowns.get(key) || 0;
if (now - lastAllowedAt < windowMs) {
return false;
}
cooldowns.set(key, now);
return true;
}
export function getCachedPayload(cache, key, ttlMs, buildPayload, now = Date.now()) {
const cached = cache.get(key);
if (cached && now - cached.createdAt < ttlMs) {
return cached.payload;
}
const payload = buildPayload();
cache.set(key, { createdAt: now, payload });
return payload;
}
export function isAdminMetricsAuthorized(authHeader, adminToken) {
if (!adminToken || typeof adminToken !== 'string') return false;
if (!authHeader || typeof authHeader !== 'string') return false;
const prefix = 'Bearer ';
if (!authHeader.startsWith(prefix)) return false;
const provided = authHeader.slice(prefix.length);
if (!provided) return false;
const expectedBuffer = Buffer.from(adminToken);
const providedBuffer = Buffer.from(provided);
if (expectedBuffer.length !== providedBuffer.length) return false;
return crypto.timingSafeEqual(expectedBuffer, providedBuffer);
}
export function isAdminMetricsTokenStrong(adminToken, minLength = 32) {
return !adminToken || (typeof adminToken === 'string' && adminToken.length >= minLength);
}
export function buildHealthPayload({
rooms,
connections,
includeMetrics = false,
now = Date.now(),
uptime = 0,
memoryUsage = () => process.memoryUsage(),
rateLimitSizes = {}
}) {
const payload = {
status: 'ok',
uptime,
rooms: rooms.size,
connections,
timestamp: now
};
if (!includeMetrics) return payload;
const roomValues = Array.from(rooms.values());
const roomSizes = roomValues.map(room => room.peers?.size || 0);
const peers = roomSizes.reduce((sum, size) => sum + size, 0);
const maxPeersInRoom = roomSizes.length > 0 ? Math.max(...roomSizes) : 0;
const avgPeersPerRoom = roomSizes.length > 0
? Math.round((peers / roomSizes.length) * 100) / 100
: 0;
const mem = memoryUsage();
return {
...payload,
peers,
roomsWithLobby: roomValues.filter(room => !!room.activeLobby).length,
avgPeersPerRoom,
maxPeersInRoom,
rateLimitEntries: {
connections: rateLimitSizes.connections || 0,
events: rateLimitSizes.events || 0,
health: rateLimitSizes.health || 0,
adminMetricsAuth: rateLimitSizes.adminMetricsAuth || 0,
authFailures: rateLimitSizes.authFailures || 0,
roomList: rateLimitSizes.roomList || 0
},
memory: {
rss: mem.rss,
heapUsed: mem.heapUsed,
heapTotal: mem.heapTotal
}
};
}
+14 -24
View File
@@ -9,7 +9,6 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"bcryptjs": "^3.0.3",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"socket.io": "^4.8.3"
@@ -70,15 +69,6 @@
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/body-parser": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
@@ -266,9 +256,9 @@
}
},
"node_modules/engine.io": {
"version": "6.6.6",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz",
"integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==",
"version": "6.6.8",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz",
"integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
@@ -280,7 +270,7 @@
"cors": "~2.8.5",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.18.3"
"ws": "~8.20.1"
},
"engines": {
"node": ">=10.2.0"
@@ -749,9 +739,9 @@
}
},
"node_modules/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -951,13 +941,13 @@
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
"integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz",
"integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==",
"license": "MIT",
"dependencies": {
"debug": "~4.4.1",
"ws": "~8.18.3"
"ws": "~8.20.1"
}
},
"node_modules/socket.io-parser": {
@@ -1079,9 +1069,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
-1
View File
@@ -11,7 +11,6 @@
"license": "ISC",
"type": "module",
"dependencies": {
"bcryptjs": "^3.0.3",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"socket.io": "^4.8.3"
+1 -2
View File
@@ -67,7 +67,6 @@ export const BLACKLIST_DOMAINS = [
// Productivity & Project Management
'atlassian.net',
'jira',
'trello.com',
'notion.so',
'monday.com',
@@ -168,7 +167,7 @@ export const BLACKLIST_DOMAINS = [
// Games & Idle Sites
'milkywayidle.com',
'melvoridle.com',
'cookieclicker.',
'orteil.dashnet.org',
'clickerheroes.com',
'kongregate.com',
'armorgames.com',
+26 -12
View File
@@ -24,12 +24,19 @@ We divide supported languages into two tiers: **Core Languages** (fully hand-cra
| Language Code | Language Name | Verification Status | Rationale / Context |
| :--- | :--- | :--- | :--- |
| `en` | **English** | `100% Manually Verified` | Primary developer language and system default |
| `de` | **German** | `100% Manually Verified` | Core market and compliance baseline |
| `fr` | **French** | `Auto-Generated` | Needs manual native review and polishing |
| `es` | **Spanish** | `Auto-Generated` | Needs manual native review and polishing |
| `pt-BR` | **Portuguese (Brasil)** | `Auto-Generated` | Needs manual native review and polishing |
| `ru` | **Russian** | `Auto-Generated` | Needs manual native review and polishing |
| `en` | 🇬🇧 **English** | `100% Manually Verified` | Primary developer language and system default |
| `de` | 🇩🇪 **German** | `100% Manually Verified` | Core market and compliance baseline |
| `fr` | 🇫🇷 **French** | `Auto-Generated` | Needs manual native review and polishing |
| `es` | 🇪🇸 **Spanish** | `Auto-Generated` | Needs manual native review and polishing |
| `pt-BR` | 🇧🇷 **Portuguese (Brasil)** | `Auto-Generated` | Needs manual native review and polishing |
| `ru` | 🇷🇺 **Russian** | `Auto-Generated` | Needs manual native review and polishing |
| `it` | 🇮🇹 **Italian** | `Auto-Generated` | Needs manual native review and polishing |
| `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** | `Auto-Generated` | Needs manual native review and polishing |
> [!WARNING]
> **Autogeneration Quality Rule**
@@ -155,12 +162,19 @@ In **v2.0**, we extended full internationalization support to the **Browser Exte
* **Locales Directory:** [`extension/locales/`](file:///Users/koala/Documents/KoalaPlay/extension/locales/)
* **Active Dictionaries:**
* [`en.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/en.json) (Baseline English)
* [`de.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/de.json) (German)
* [`fr.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/fr.json) (French)
* [`es.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/es.json) (Spanish)
* [`pt-BR.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/pt-BR.json) (Portuguese (Brasil))
* [`ru.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/ru.json) (Russian)
* [`en.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/en.json) (🇬🇧 Baseline English)
* [`de.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/de.json) (🇩🇪 German)
* [`fr.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/fr.json) (🇫🇷 French)
* [`es.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/es.json) (🇪🇸 Spanish)
* [`pt-BR.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/pt-BR.json) (🇧🇷 Portuguese (Brasil))
* [`ru.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/ru.json) (🇷🇺 Russian)
* [`it.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/it.json) (🇮🇹 Italian)
* [`pl.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/pl.json) (🇵🇱 Polish)
* [`tr.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/tr.json) (🇹🇷 Turkish)
* [`nl.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/nl.json) (🇳🇱 Dutch)
* [`ja.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/ja.json) (🇯🇵 Japanese)
* [`ko.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/ko.json) (🇰🇷 Korean)
* [`pt.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/pt.json) (🇵🇹 European Portuguese)
* **Translation Engine:** [`extension/i18n.js`](file:///Users/koala/Documents/KoalaPlay/extension/i18n.js)
* **Validation Script:** [`scripts/test-locales.js`](file:///Users/koala/Documents/KoalaPlay/scripts/test-locales.js)
+83 -12
View File
@@ -1,6 +1,22 @@
// KoalaSync Landing Page Logic
document.addEventListener('DOMContentLoaded', () => {
const safeGetLocalStorage = (key) => {
try {
return localStorage.getItem(key);
} catch (_) {
return null;
}
};
const safeSetLocalStorage = (key, val) => {
try {
localStorage.setItem(key, val);
} catch (_) {
return;
}
};
// Scroll Reveal Logic (IntersectionObserver for performance)
const revealElements = document.querySelectorAll('[data-reveal]');
@@ -310,7 +326,7 @@ document.addEventListener('DOMContentLoaded', () => {
const updateDynamicVersion = async () => {
try {
const versionPath = document.documentElement.lang === 'de' ? '../version.json' : 'version.json';
const versionPath = document.documentElement.lang === 'en' ? 'version.json' : '../version.json';
const response = await fetch(versionPath);
if (!response.ok) return;
const data = await response.json();
@@ -456,14 +472,14 @@ document.addEventListener('DOMContentLoaded', () => {
// Dynamically localize home links on root dynamic pages (impressum, datenschutz, join)
const localizeHomeLinks = () => {
const activeLang = localStorage.getItem('koala_lang') || (navigator.language.startsWith('de') ? 'de' : 'en');
const activeLang = safeGetLocalStorage('koala_lang') || (navigator.language.startsWith('de') ? 'de' : 'en');
const path = window.location.pathname;
const pathSegments = path.split('/');
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'pt-BR', 'ru'].includes(seg));
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'].includes(seg));
// Only need to do this dynamic rewrite if we are NOT already inside a localized subdirectory
if (!isSubdir) {
const homeLinks = document.querySelectorAll('a[href="./"], a[href="de/"], a[href="fr/"], a[href="es/"], a[href="pt-BR/"], a[href="ru/"]');
const homeLinks = document.querySelectorAll('a[href="./"], a[href="de/"], a[href="fr/"], a[href="es/"], a[href="pt-BR/"], a[href="ru/"], a[href="it/"], a[href="pl/"], a[href="tr/"], a[href="nl/"], a[href="ja/"], a[href="ko/"], a[href="pt/"]');
homeLinks.forEach(link => {
link.href = (activeLang === 'en') ? './' : `${activeLang}/`;
});
@@ -477,16 +493,44 @@ document.addEventListener('DOMContentLoaded', () => {
const path = window.location.pathname;
// Save the user's preference
localStorage.setItem('koala_lang', newLang);
safeSetLocalStorage('koala_lang', newLang);
const isLegalImprint = path.includes('impressum') || path.includes('imprint');
const isLegalPrivacy = path.includes('datenschutz') || path.includes('privacy');
if (isLegalImprint) {
let target;
const hasHtml = path.endsWith('.html');
if (newLang === 'de') {
target = hasHtml ? 'de/impressum.html' : 'de/impressum';
if (path.includes('/de/')) target = hasHtml ? 'impressum.html' : 'impressum';
} else {
target = hasHtml ? 'imprint.html' : 'imprint';
if (path.includes('/de/')) target = hasHtml ? '../imprint.html' : '../imprint';
}
window.location.href = target;
return;
} else if (isLegalPrivacy) {
let target;
const hasHtml = path.endsWith('.html');
if (newLang === 'de') {
target = hasHtml ? 'de/datenschutz.html' : 'de/datenschutz';
if (path.includes('/de/')) target = hasHtml ? 'datenschutz.html' : 'datenschutz';
} else {
target = hasHtml ? 'privacy.html' : 'privacy';
if (path.includes('/de/')) target = hasHtml ? '../privacy.html' : '../privacy';
}
window.location.href = target;
return;
}
// Determine if we are on a static landing page versus a dynamic utility page
const isLegalOrJoin = path.includes('impressum') || path.includes('datenschutz') || path.includes('join');
const isIndex = !isLegalOrJoin;
const isIndex = !path.includes('join');
if (isIndex) {
// Static navigation: Route to correct subdirectory
const pathSegments = path.split('/');
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'pt-BR', 'ru'].includes(seg));
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'].includes(seg));
let targetPath;
if (newLang === 'en') {
@@ -509,7 +553,7 @@ document.addEventListener('DOMContentLoaded', () => {
} else {
// Dynamic page: Toggle classes and update elements dynamically without navigating away
const html = document.documentElement;
html.classList.remove('lang-en', 'lang-de', 'lang-fr', 'lang-es', 'lang-pt-br', 'lang-ru');
html.classList.remove('lang-en', 'lang-de', 'lang-fr', 'lang-es', 'lang-pt-br', 'lang-ru', 'lang-it', 'lang-pl', 'lang-tr', 'lang-nl', 'lang-ja', 'lang-ko', 'lang-pt');
// Fallback dynamic pages to 'en' if 'de' is not chosen (since fr/es markup is not present)
const activeDisplayLang = (newLang === 'de') ? 'de' : 'en';
@@ -533,20 +577,47 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
// Dynamically adjust language select width to fit the selected option's text length
const adjustDropdownWidth = () => {
document.querySelectorAll('.lang-dropdown').forEach(select => {
const tempSpan = document.createElement('span');
const style = window.getComputedStyle(select);
tempSpan.style.fontFamily = style.fontFamily;
tempSpan.style.fontSize = style.fontSize;
tempSpan.style.fontWeight = style.fontWeight;
tempSpan.style.visibility = 'hidden';
tempSpan.style.position = 'absolute';
tempSpan.style.whiteSpace = 'nowrap';
const activeOption = select.options[select.selectedIndex];
if (activeOption) {
tempSpan.textContent = activeOption.textContent;
document.body.appendChild(tempSpan);
const textWidth = tempSpan.getBoundingClientRect().width;
select.style.width = (textWidth + 18) + 'px';
document.body.removeChild(tempSpan);
}
});
};
// Register change event listener for the dropdowns
document.querySelectorAll('.lang-dropdown').forEach(select => {
select.addEventListener('change', handleLanguageChange);
select.addEventListener('change', (e) => {
handleLanguageChange(e);
adjustDropdownWidth();
});
});
// Initialize language select elements to show the current preferred language
const initLanguageSelectorValue = () => {
const savedLang = localStorage.getItem('koala_lang');
const savedLang = safeGetLocalStorage('koala_lang');
const browserLang = navigator.language.startsWith('de') ? 'de' : 'en';
const activePref = savedLang || browserLang;
document.querySelectorAll('.lang-dropdown').forEach(select => {
select.value = activePref;
});
adjustDropdownWidth();
};
// Impressum Email Obfuscation Click Reveal
@@ -555,7 +626,7 @@ document.addEventListener('DOMContentLoaded', () => {
const user = this.getAttribute('data-user');
const domain = this.getAttribute('data-domain');
if (user && domain) {
this.innerHTML = `${user}@${domain}`;
this.textContent = `${user}@${domain}`;
}
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

+69 -7
View File
@@ -72,6 +72,51 @@ async function compile() {
const wwwDir = path.join(websiteDir, 'www');
fs.mkdirSync(wwwDir, { recursive: true });
// ── 0. Auto-generate website logo sizes and sync favicons ──
console.log('Generating responsive website logos...');
const rawLogoSrc = path.join(websiteDir, '..', 'assets', 'icon', 'TwoPointZero_Logo_Icon_600.webp');
const targetAssetsDir = path.join(websiteDir, 'assets');
if (fs.existsSync(rawLogoSrc)) {
fs.mkdirSync(targetAssetsDir, { recursive: true });
// Generate NewLogoIcon_64.webp (64x64)
await sharp(rawLogoSrc)
.resize(64, 64)
.toFile(path.join(targetAssetsDir, 'NewLogoIcon_64.webp'));
// Generate NewLogoIcon_128.webp (128x128)
await sharp(rawLogoSrc)
.resize(128, 128)
.toFile(path.join(targetAssetsDir, 'NewLogoIcon_128.webp'));
// Generate NewLogoIcon.webp (256x256)
await sharp(rawLogoSrc)
.resize(256, 256)
.toFile(path.join(targetAssetsDir, 'NewLogoIcon.webp'));
console.log(' ✓ WebP logo variants successfully generated in website/assets/');
} else {
console.warn(` ⚠️ Warning: Source logo ${rawLogoSrc} not found. Skipping auto-generation.`);
}
const pngMappings = [
{ src: 'TwoPointZero_Logo_Icon_16.png', dest: 'favicon-16x16.png' },
{ src: 'TwoPointZero_Logo_Icon_32.png', dest: 'favicon-32x32.png' },
{ src: 'TwoPointZero_Logo_Icon_256.png', dest: 'apple-touch-icon.png' },
{ src: 'TwoPointZero_Logo_Icon_256.png', dest: 'icon-192x192.png' }
];
for (const mapping of pngMappings) {
const srcPath = path.join(websiteDir, '..', 'assets', 'icon', mapping.src);
const destPath = path.join(targetAssetsDir, mapping.dest);
if (fs.existsSync(srcPath)) {
fs.copyFileSync(srcPath, destPath);
} else {
console.warn(` ⚠️ Warning: Source PNG ${srcPath} not found.`);
}
}
console.log(' ✓ Favicons/touch icons successfully synced to website/assets/');
// ── 1. Minify CSS/JS (must happen first so hashes go into HTML) ──
console.log('Minifying CSS/JS...');
const styleRaw = fs.readFileSync(path.join(websiteDir, 'style.css'), 'utf8');
@@ -117,13 +162,18 @@ async function compile() {
}
const templateContent = fs.readFileSync(templatePath, 'utf8');
const localesDir = path.join(websiteDir, 'locales');
const languages = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru'];
const languages = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'];
// Read version for build-time injection (SEO: crawlers see real version)
const versionJson = JSON.parse(fs.readFileSync(path.join(websiteDir, 'version.json'), 'utf8'));
const buildVersion = versionJson.version || '?';
const englishHtml = {}; // track for join-page cross-ref
function compilePage(locale, assetPath, lang) {
let compiled = templateContent;
compiled = compiled.replace(/\{\{ASSET_PATH\}\}/g, assetPath);
compiled = compiled.replace(/\{\{VERSION\}\}/g, buildVersion);
languages.forEach(l => {
compiled = compiled.replace(new RegExp(`\\{\\{SELECTED_${l.toUpperCase()}\\}\\}`, 'g'), l === lang ? 'selected' : '');
});
@@ -153,13 +203,25 @@ async function compile() {
}
}
// ── 4. Copy static HTML files (join, impressum, datenschutz) ──
// ── 4. Copy static HTML files ──
console.log('Copying static pages...');
const staticHtml = ['join.html', 'impressum.html', 'datenschutz.html'];
for (const file of staticHtml) {
const src = path.join(websiteDir, file);
const dest = path.join(wwwDir, file);
if (fs.existsSync(src)) { fs.copyFileSync(src, dest); console.log(` Copied: ${file}`); }
fs.mkdirSync(path.join(wwwDir, 'de'), { recursive: true });
const staticMappings = [
{ src: 'join.html', dest: 'join.html' },
{ src: 'imprint.html', dest: 'imprint.html' },
{ src: 'privacy.html', dest: 'privacy.html' },
{ src: 'impressum-de.html', dest: 'de/impressum.html' },
{ src: 'datenschutz-de.html', dest: 'de/datenschutz.html' },
{ src: 'impressum.html', dest: 'impressum.html' },
{ src: 'datenschutz.html', dest: 'datenschutz.html' }
];
for (const mapping of staticMappings) {
const src = path.join(websiteDir, mapping.src);
const dest = path.join(wwwDir, mapping.dest);
if (fs.existsSync(src)) {
fs.copyFileSync(src, dest);
console.log(` Copied: ${mapping.src}${mapping.dest}`);
}
}
// ── 5. Copy generic static files ──
+169
View File
@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="de" class="lang-de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutz | KoalaSync</title>
<link rel="preload" href="../style.min.css" as="style">
<link rel="stylesheet" href="../style.min.css">
<link rel="icon" type="image/webp" href="../assets/NewLogoIcon_64.webp">
<meta name="robots" content="noindex">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Startseite", "item": "https://sync.koalastuff.net/de/" },
{ "@type": "ListItem", "position": 2, "name": "Datenschutz", "item": "https://sync.koalastuff.net/de/datenschutz" }
]
}
</script>
<!-- Mobile Browser Theme Styling -->
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<script src="../lang-init.min.js"></script>
</head>
<body>
<div class="bg-blobs">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<nav>
<div class="container nav-content">
<a href="./" class="logo-area" style="text-decoration: none;">
<img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span>
</a>
<div class="nav-links">
<a href="./" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width: 16px; height: 16px; display: block;"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<span>Startseite</span>
</a>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
GitHub
</a>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="globe-icon"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
<option value="en">🇬🇧 English</option>
<option value="de">🇩🇪 Deutsch</option>
<option value="fr">🇫🇷 Français</option>
<option value="es">🇪🇸 Español</option>
<option value="pt-BR">🇧🇷 Português (Brasil)</option>
<option value="ru">🇷🇺 Русский</option>
<option value="it">🇮🇹 Italiano</option>
<option value="pl">🇵🇱 Polski</option>
<option value="tr">🇹🇷 Türkçe</option>
<option value="nl">🇳🇱 Nederlands</option>
<option value="ja">🇯🇵 日本語</option>
<option value="ko">🇰🇷 한국어</option>
<option value="pt">🇵🇹 Português (Portugal)</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
</div>
</div>
</nav>
<main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;">
<img src="../assets/KoalaPrivacy.webp" alt="Niedlicher Koala, der den Datenschutz repräsentiert" class="legal-mascot" width="180" height="180">
</div>
<h1>Datenschutz</h1>
<p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
Sicherheit & Privatsphäre
</p>
<section>
<h2>1. Hosting & Logfiles</h2>
<p>
Diese Seite wird auf einem privaten Server gehostet. Zur Gewährleistung der Stabilität werden standardmäßige Server-Logs (IP, Browser, Zeit) erhoben, aber nicht mit Personen verknüpft und nach 7 Tagen automatisch gelöscht.
</p>
</section>
<section>
<h2>2. Keine Drittanbieter & Open Source</h2>
<p>
KoalaSync verzichtet bewusst auf Analyse-Tools, Tracking-Cookies oder Werbenetzwerke. Wir laden keine Ressourcen von Drittanbietern (wie Google Fonts) nach, um Ihre Privatsphäre maximal zu schützen.
</p>
<p style="margin-top: 0.5rem;">
Da KoalaSync vollständig Open Source ist, kann zudem jede Zeile Code auf unserem <a href="https://github.com/shik3i/KoalaSync" target="_blank" style="color: var(--accent);">GitHub-Repository</a> öffentlich eingesehen und auf Sicherheit geprüft werden.
</p>
</section>
<section>
<h2>3. Relay-Server Architektur</h2>
<p>
Unser Relay-Server arbeitet ausschließlich im Arbeitsspeicher (RAM). Nachrichten zwischen Teilnehmern werden nicht auf Festplatten gespeichert und sind flüchtig. Sobald ein Raum geschlossen wird, werden alle zugehörigen Metadaten sofort gelöscht. Wir tracken Sie nicht. Wir überwachen lediglich unseren Server auf Basis von aggregierten, anonymen, nicht-personenbezogenen System-Leistungsmetriken.
</p>
</section>
<section>
<h2>4. Browser-Erweiterung (Extension)</h2>
<p>
Um die geräteübergreifende Synchronisation zu ermöglichen, erfasst die KoalaSync Browser-Erweiterung temporär Daten des aktuell aktiven Video-Tabs (z. B. Tab-Titel, Medien-Metadaten wie den Videotitel sowie den Wiedergabestatus). Diese Daten werden ausschließlich zur Synchronisation an die anderen Teilnehmer in Ihrem Raum gesendet. Es wird ausdrücklich <strong>kein allgemeiner Browserverlauf (Browsing History)</strong> ausgelesen, gespeichert oder übermittelt.
</p>
</section>
<section>
<h2>5. Berechtigungen der Erweiterung</h2>
<p>
Um ihre technischen Zweck zu erfüllen, benötigt die Browser-Erweiterung bestimmte Berechtigungen. Jede Berechtigung wird ausschließlich für die Kernfunktionalität genutzt:
</p>
<ul style="margin-left: 1.5rem; margin-top: 0.5rem; color: var(--text-muted); font-size: 0.9rem; list-style-type: disc; display: flex; flex-direction: column; gap: 0.35rem;">
<li><strong>storage</strong>: Erlaubt das lokale Speichern von Benutzernamen, Server-URLs und Raum-Zugangsdaten im Browser, damit Sie sich nicht jedes Mal neu anmelden müssen.</li>
<li><strong>tabs</strong>: Wird benötigt, um geöffnete Tabs im Dropdown der Erweiterung aufzulisten und deren Titel auszulesen, damit Sie bequem den richtigen Video-Tab auswählen können.</li>
<li><strong>scripting</strong>: Erforderlich, um das Synchronisationsskript (content.js) sicher in den von Ihnen ausgewählten Videotab zu injizieren.</li>
<li><strong>alarms</strong>: Verhindert, dass der Hintergrunddienst der Erweiterung (Service Worker) während einer aktiven Synchronisationssitzung vom Browser in den Ruhezustand versetzt wird.</li>
<li><strong>activeTab</strong>: Ermöglicht eine sichere, temporäre Interaktion mit dem momentan aktiven Tab für direkte Steuerungsbefehle.</li>
<li><strong>notifications</strong>: Ermöglicht optionale Desktop-Benachrichtigungen, wenn beispielsweise ein neuer Freund dem Raum beitritt.</li>
<li><strong>&lt;all_urls&gt; (Host-Berechtigung)</strong>: Ermöglicht der Erweiterung, auf beliebigen Webseiten nach HTML5-Videoelementen zu suchen, damit die Synchronisation plattformübergreifend (z. B. auf YouTube, Netflix, Jellyfin etc.) funktioniert.</li>
</ul>
</section>
<section>
<h2>6. Brute-Force Schutz</h2>
<p>
Zur Sicherheit unserer Nutzer speichern wir fehlgeschlagene Login-Versuche (IP-Adresse und Raum-ID) für maximal 15 Minuten im RAM, um automatisierte Angriffe zu verhindern. Diese Daten werden danach rückstandslos gelöscht.
</p>
</section>
<section>
<h2>7. Ihre Rechte</h2>
<p>
Sie haben das Recht auf Auskunft, Berichtigung oder Löschung Ihrer Daten. Da wir jedoch keine personenbezogenen Daten dauerhaft speichern, ist eine Zuordnung zu Ihrer Person in der Regel technisch nicht möglich.
</p>
<p>Kontakt bei Fragen: <span class="email-reveal" data-user="koalasync_datenschutz" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[E-Mail anzeigen]</span></p>
</section>
</div>
</main>
<footer>
<div class="container">
<p>&copy; 2026 KoalaSync. Open Source unter der MIT-Lizenz.</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="impressum" style="color: var(--text-muted); text-decoration: none;">Impressum</a>
<a href="datenschutz" style="color: var(--text-muted); text-decoration: none;">Datenschutz</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
Mastodon
</a>
</div>
</div>
</footer>
<script src="../app.min.js"></script>
</body>
</html>
+15 -212
View File
@@ -2,220 +2,23 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutz / Privacy Policy | KoalaSync</title>
<link rel="preload" href="style.min.css" as="style">
<link rel="stylesheet" href="style.min.css">
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
<meta name="robots" content="noindex">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://sync.koalastuff.net/" },
{ "@type": "ListItem", "position": 2, "name": "Privacy Policy", "item": "https://sync.koalastuff.net/datenschutz" }
]
}
<title>Redirecting...</title>
<script>
(function() {
var savedLang = localStorage.getItem('koala_lang');
var browserLang = (navigator.language || navigator.userLanguage || '').toLowerCase();
var isDe = (savedLang === 'de') || (!savedLang && browserLang.indexOf('de') === 0);
var path = window.location.pathname;
var hasHtml = path.endsWith('.html');
if (isDe) {
window.location.replace(hasHtml ? 'de/datenschutz.html' : 'de/datenschutz');
} else {
window.location.replace(hasHtml ? 'privacy.html' : 'privacy');
}
})();
</script>
<!-- Mobile Browser Theme Styling -->
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<script src="lang-init.min.js"></script>
</head>
<body>
<div class="bg-blobs">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<nav>
<div class="container nav-content">
<a href="./" class="logo-area" style="text-decoration: none;">
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span>
</a>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="./" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width: 16px; height: 16px; display: block;"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<span lang="de">Startseite</span><span lang="en">Home</span>
</a>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
GitHub
</a>
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="globe-icon"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="ru">Русский</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
</div>
</div>
</div>
</nav>
<main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;">
<img src="assets/KoalaPrivacy.webp" alt="Cute Koala representing privacy and data security" class="legal-mascot" lang="en" width="180" height="180">
<img src="assets/KoalaPrivacy.webp" alt="Niedlicher Koala, der den Datenschutz repräsentiert" class="legal-mascot" lang="de" width="180" height="180">
</div>
<h1 lang="de">Datenschutz</h1>
<h1 lang="en">Privacy Policy</h1>
<p lang="de" style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
Sicherheit & Privatsphäre
</p>
<p lang="en" style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
Security & Privacy
</p>
<section>
<h2>
<span lang="de">1. Hosting & Logfiles</span>
<span lang="en">1. Hosting & Logfiles</span>
</h2>
<p lang="de">
Diese Seite wird auf einem privaten Server gehostet. Zur Gewährleistung der Stabilität werden standardmäßige Server-Logs (IP, Browser, Zeit) erhoben, aber nicht mit Personen verknüpft und nach 7 Tagen automatisch gelöscht.
</p>
<p lang="en">
This site is hosted on a private server. To ensure stability, standard server logs (IP, browser, time) are collected, but not linked to individuals and are automatically deleted after 7 days.
</p>
</section>
<section>
<h2>
<span lang="de">2. Keine Drittanbieter & Open Source</span>
<span lang="en">2. No Third Parties & Open Source</span>
</h2>
<p lang="de">
KoalaSync verzichtet bewusst auf Analyse-Tools, Tracking-Cookies oder Werbenetzwerke. Wir laden keine Ressourcen von Drittanbietern (wie Google Fonts) nach, um Ihre Privatsphäre maximal zu schützen.
</p>
<p lang="de" style="margin-top: 0.5rem;">
Da KoalaSync vollständig Open Source ist, kann zudem jede Zeile Code auf unserem <a href="https://github.com/shik3i/KoalaSync" target="_blank" style="color: var(--accent);">GitHub-Repository</a> öffentlich eingesehen und auf Sicherheit geprüft werden.
</p>
<p lang="en">
KoalaSync deliberately avoids analytics tools, tracking cookies, or advertising networks. We do not load any third-party resources (such as Google Fonts) to maximize the protection of your privacy.
</p>
<p lang="en" style="margin-top: 0.5rem;">
Since KoalaSync is 100% open-source, every single line of code can also be publicly viewed and audited for security on our <a href="https://github.com/shik3i/KoalaSync" target="_blank" style="color: var(--accent);">GitHub repository</a>.
</p>
</section>
<section>
<h2>
<span lang="de">3. Relay-Server Architektur</span>
<span lang="en">3. Relay Server Architecture</span>
</h2>
<p lang="de">
Unser Relay-Server arbeitet ausschließlich im Arbeitsspeicher (RAM). Nachrichten zwischen Teilnehmern werden nicht auf Festplatten gespeichert und sind flüchtig. Sobald ein Raum geschlossen wird, werden alle zugehörigen Metadaten sofort gelöscht.
</p>
<p lang="en">
Our relay server operates exclusively in memory (RAM). Messages between participants are not stored on hard drives and are volatile. As soon as a room is closed, all associated metadata is immediately deleted.
</p>
</section>
<section>
<h2>
<span lang="de">4. Browser-Erweiterung (Extension)</span>
<span lang="en">4. Browser Extension</span>
</h2>
<p lang="de">
Um die geräteübergreifende Synchronisation zu ermöglichen, erfasst die KoalaSync Browser-Erweiterung temporär Daten des aktuell aktiven Video-Tabs (z. B. Tab-Titel, Medien-Metadaten wie den Videotitel sowie den Wiedergabestatus). Diese Daten werden ausschließlich zur Synchronisation an die anderen Teilnehmer in Ihrem Raum gesendet. Es wird ausdrücklich <strong>kein allgemeiner Browserverlauf (Browsing History)</strong> ausgelesen, gespeichert oder übermittelt.
</p>
<p lang="en">
To enable cross-device synchronization, the KoalaSync browser extension temporarily captures data from the currently active video tab (e.g., tab title, media metadata like the video title, and playback state). This data is exclusively sent to other participants in your room for synchronization. We explicitly <strong>do not read, store, or transmit your general browsing history</strong>.
</p>
</section>
<section>
<h2>
<span lang="de">5. Berechtigungen der Erweiterung</span>
<span lang="en">5. Extension Permissions</span>
</h2>
<p lang="de">
Um ihren technischen Zweck zu erfüllen, benötigt die Browser-Erweiterung bestimmte Berechtigungen. Jede Berechtigung wird ausschließlich für die Kernfunktionalität genutzt:
</p>
<ul lang="de" style="margin-left: 1.5rem; margin-top: 0.5rem; color: var(--text-muted); font-size: 0.9rem; list-style-type: disc; display: flex; flex-direction: column; gap: 0.35rem;">
<li><strong>storage</strong>: Erlaubt das lokale Speichern von Benutzernamen, Server-URLs und Raum-Zugangsdaten im Browser, damit Sie sich nicht jedes Mal neu anmelden müssen.</li>
<li><strong>tabs</strong>: Wird benötigt, um geöffnete Tabs im Dropdown der Erweiterung aufzulisten und deren Titel auszulesen, damit Sie bequem den richtigen Video-Tab auswählen können.</li>
<li><strong>scripting</strong>: Erforderlich, um das Synchronisationsskript (content.js) sicher in den von Ihnen ausgewählten Videotab zu injizieren.</li>
<li><strong>alarms</strong>: Verhindert, dass der Hintergrunddienst der Erweiterung (Service Worker) während einer aktiven Synchronisationssitzung vom Browser in den Ruhezustand versetzt wird.</li>
<li><strong>activeTab</strong>: Ermöglicht eine sichere, temporäre Interaktion mit dem momentan aktiven Tab für direkte Steuerungsbefehle.</li>
<li><strong>notifications</strong>: Ermöglicht optionale Desktop-Benachrichtigungen, wenn beispielsweise ein neuer Freund dem Raum beitritt.</li>
<li><strong>&lt;all_urls&gt; (Host-Berechtigung)</strong>: Ermöglicht der Erweiterung, auf beliebigen Webseiten nach HTML5-Videoelementen zu suchen, damit die Synchronisation plattformübergreifend (z. B. auf YouTube, Netflix, Jellyfin etc.) funktioniert.</li>
</ul>
<p lang="en" style="margin-top: 1.5rem;">
To fulfill its technical purpose, the browser extension requires certain permissions. Each permission is used exclusively for core functionality:
</p>
<ul lang="en" style="margin-left: 1.5rem; margin-top: 0.5rem; color: var(--text-muted); font-size: 0.9rem; list-style-type: disc; display: flex; flex-direction: column; gap: 0.35rem;">
<li><strong>storage</strong>: Allows local storage of your username, server URL, and room credentials in your browser so you don't have to log in every time.</li>
<li><strong>tabs</strong>: Required to list open tabs in the extension's dropdown and read their titles, making it easy for you to select the correct video tab.</li>
<li><strong>scripting</strong>: Required to securely inject the synchronization script (content.js) into your selected video tab.</li>
<li><strong>alarms</strong>: Prevents the extension's background service worker from being suspended by the browser during an active synchronization session.</li>
<li><strong>activeTab</strong>: Enables secure, temporary interaction with the currently active tab for direct playback commands.</li>
<li><strong>notifications</strong>: Enables optional desktop notifications, such as when a new friend joins the room.</li>
<li><strong>&lt;all_urls&gt; (Host permission)</strong>: Allows the extension to scan for HTML5 video elements on any website, enabling cross-platform synchronization (e.g., on YouTube, Netflix, Jellyfin etc.).</li>
</ul>
</section>
<section>
<h2>
<span lang="de">6. Brute-Force Schutz</span>
<span lang="en">6. Brute-Force Protection</span>
</h2>
<p lang="de">
Zur Sicherheit unserer Nutzer speichern wir fehlgeschlagene Login-Versuche (IP-Adresse und Raum-ID) für maximal 15 Minuten im RAM, um automatisierte Angriffe zu verhindern. Diese Daten werden danach rückstandslos gelöscht.
</p>
<p lang="en">
For the security of our users, we store failed login attempts (IP address and room ID) for a maximum of 15 minutes in RAM to prevent automated attacks. This data is deleted without a trace afterwards.
</p>
</section>
<section>
<h2>
<span lang="de">7. Ihre Rechte</span>
<span lang="en">7. Your Rights</span>
</h2>
<p lang="de">
Sie haben das Recht auf Auskunft, Berichtigung oder Löschung Ihrer Daten. Da wir jedoch keine personenbezogenen Daten dauerhaft speichern, ist eine Zuordnung zu Ihrer Person in der Regel technisch nicht möglich.
</p>
<p lang="en">
You have the right to information, correction, or deletion of your data. However, since we do not store any personal data permanently, linking data to your person is technically impossible in most cases.
</p>
<p lang="de">Kontakt bei Fragen: <span class="email-reveal" data-user="koalasync_datenschutz" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[E-Mail anzeigen]</span></p>
<p lang="en">Contact for questions: <span class="email-reveal" data-user="koalasync_datenschutz" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[Show Email]</span></p>
</section>
</div>
</main>
<footer>
<div class="container">
<p>&copy; 2026 KoalaSync. Open source under the MIT License.</p>
<p lang="de" style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
<p lang="en" style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="impressum" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Impressum</span><span lang="en">Legal Notice</span></a>
<a href="datenschutz" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Datenschutz</span><span lang="en">Privacy Policy</span></a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
Mastodon
</a>
</div>
</div>
</footer>
<script src="app.min.js"></script>
<p>Redirecting / Weiterleitung...</p>
</body>
</html>
+156
View File
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="de" class="lang-de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impressum | KoalaSync</title>
<link rel="preload" href="../style.min.css" as="style">
<link rel="stylesheet" href="../style.min.css">
<link rel="icon" type="image/webp" href="../assets/NewLogoIcon_64.webp">
<meta name="robots" content="noindex">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Startseite", "item": "https://sync.koalastuff.net/de/" },
{ "@type": "ListItem", "position": 2, "name": "Impressum", "item": "https://sync.koalastuff.net/de/impressum" }
]
}
</script>
<!-- Mobile Browser Theme Styling -->
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<script src="../lang-init.min.js"></script>
</head>
<body>
<div class="bg-blobs">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<nav>
<div class="container nav-content">
<a href="./" class="logo-area" style="text-decoration: none;">
<img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span>
</a>
<div class="nav-links">
<a href="./" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width: 16px; height: 16px; display: block;"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<span>Startseite</span>
</a>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
GitHub
</a>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="globe-icon"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
<option value="en">🇬🇧 English</option>
<option value="de">🇩🇪 Deutsch</option>
<option value="fr">🇫🇷 Français</option>
<option value="es">🇪🇸 Español</option>
<option value="pt-BR">🇧🇷 Português (Brasil)</option>
<option value="ru">🇷🇺 Русский</option>
<option value="it">🇮🇹 Italiano</option>
<option value="pl">🇵🇱 Polski</option>
<option value="tr">🇹🇷 Türkçe</option>
<option value="nl">🇳🇱 Nederlands</option>
<option value="ja">🇯🇵 日本語</option>
<option value="ko">🇰🇷 한국어</option>
<option value="pt">🇵🇹 Português (Portugal)</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
</div>
</div>
</nav>
<main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;">
<img src="../assets/KoalaImprintl.webp" alt="Niedlicher Koala, der das Impressum repräsentiert" class="legal-mascot" width="180" height="180">
</div>
<h1>Impressum</h1>
<p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
Transparenz & Identifikation
</p>
<section>
<h2>Betreiber & Kontakt</h2>
<p>Timo (KoalaDev) Privatperson</p>
<p style="margin-top: 0.5rem;">
E-Mail: <span class="email-reveal" data-user="koalasync_admin" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[E-Mail anzeigen]</span>
</p>
<p style="margin-top: 0.75rem;">
<span style="opacity: 0.6;">🔒 Private Nachricht:</span>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--accent); text-decoration: none;">@koalastuff auf Mastodon</a>
</p>
<p style="margin-top: 0.5rem;">
<span style="opacity: 0.6;">🐛 Active Bedenken / Bug Reports:</span>
<a href="https://github.com/Shik3i/KoalaSync/issues" target="_blank" style="color: var(--accent); text-decoration: none;">GitHub Issues</a>
</p>
</section>
<section style="opacity: 0.8;">
<h2>Privatprojekt-Hinweis</h2>
<p>
Diese Website ist ein rein privates Hobby-Projekt und dient keinen geschäftsmäßigen Zwecken.
Eine Impressumspflicht nach § 5 DDG (ehemals TMG) besteht daher nicht.
Diese Angaben erfolgen rein freiwillig zur Transparenz gegenüber der Community.
</p>
</section>
<section>
<h2>Haftung für Inhalte</h2>
<p>
Gemäß § 7 Abs.1 DDG sind wir für eigene Inhalte verantwortlich. Nach §§ 8 bis 10 DDG sind wir jedoch nicht verpflichtet,
übermittelte oder gespeicherte fremde Informationen zu überwachen.
</p>
</section>
<section>
<h2>Haftung für Links</h2>
<p>
Unser Angebot enthält Links zu externen Websites Dritter. Auf deren Inhalte haben wir keinen Einfluss und
können daher keine Gewähr für diese fremden Inhalte übernehmen.
</p>
</section>
<section>
<h2>Urheberrecht</h2>
<p>
Die durch die Seitenbetreiber erstellten Inhalte auf diesen Seiten unterliegen dem deutschen Urheberrecht.
Vervielfältigung, Bearbeitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung.
</p>
</section>
</div>
</main>
<footer>
<div class="container">
<p>&copy; 2026 KoalaSync. Open Source unter der MIT-Lizenz.</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="impressum" style="color: var(--text-muted); text-decoration: none;">Impressum</a>
<a href="datenschutz" style="color: var(--text-muted); text-decoration: none;">Datenschutz</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
Mastodon
</a>
</div>
</div>
</footer>
<script src="../app.min.js"></script>
</body>
</html>
+15 -186
View File
@@ -2,194 +2,23 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impressum / Legal Notice | KoalaSync</title>
<link rel="preload" href="style.min.css" as="style">
<link rel="stylesheet" href="style.min.css">
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
<meta name="robots" content="noindex">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://sync.koalastuff.net/" },
{ "@type": "ListItem", "position": 2, "name": "Legal Notice", "item": "https://sync.koalastuff.net/impressum" }
]
}
<title>Redirecting...</title>
<script>
(function() {
var savedLang = localStorage.getItem('koala_lang');
var browserLang = (navigator.language || navigator.userLanguage || '').toLowerCase();
var isDe = (savedLang === 'de') || (!savedLang && browserLang.indexOf('de') === 0);
var path = window.location.pathname;
var hasHtml = path.endsWith('.html');
if (isDe) {
window.location.replace(hasHtml ? 'de/impressum.html' : 'de/impressum');
} else {
window.location.replace(hasHtml ? 'imprint.html' : 'imprint');
}
})();
</script>
<!-- Mobile Browser Theme Styling -->
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<script src="lang-init.min.js"></script>
</head>
<body>
<div class="bg-blobs">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<nav>
<div class="container nav-content">
<a href="./" class="logo-area" style="text-decoration: none;">
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span>
</a>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="./" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width: 16px; height: 16px; display: block;"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<span lang="de">Startseite</span><span lang="en">Home</span>
</a>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
GitHub
</a>
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="globe-icon"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="ru">Русский</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
</div>
</div>
</div>
</nav>
<main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;">
<img src="assets/KoalaImprintl.webp" alt="Cute Koala representing the legal notice page" class="legal-mascot" lang="en" width="180" height="180">
<img src="assets/KoalaImprintl.webp" alt="Niedlicher Koala, der das Impressum repräsentiert" class="legal-mascot" lang="de" width="180" height="180">
</div>
<h1 lang="de">Impressum</h1>
<h1 lang="en">Legal Notice</h1>
<p lang="de" style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
Transparenz & Identifikation
</p>
<p lang="en" style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
Transparency & Identification
</p>
<section>
<h2>
<span lang="de">Betreiber & Kontakt</span>
<span lang="en">Operator & Contact</span>
</h2>
<p lang="de">Timo (KoalaDev) Privatperson</p>
<p lang="en">Timo (KoalaDev) Private Individual</p>
<p lang="de" style="margin-top: 0.5rem;">
E-Mail: <span class="email-reveal" data-user="koalasync_admin" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[E-Mail anzeigen]</span>
</p>
<p lang="en" style="margin-top: 0.5rem;">
E-Mail: <span class="email-reveal" data-user="koalasync_admin" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[Show Email]</span>
</p>
<p lang="de" style="margin-top: 0.75rem;">
<span style="opacity: 0.6;">🔒 Private Nachricht:</span>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--accent); text-decoration: none;">@koalastuff auf Mastodon</a>
</p>
<p lang="en" style="margin-top: 0.75rem;">
<span style="opacity: 0.6;">🔒 Private message:</span>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--accent); text-decoration: none;">@koalastuff on Mastodon</a>
</p>
<p lang="de" style="margin-top: 0.5rem;">
<span style="opacity: 0.6;">🐛 Active Bedenken / Bug Reports:</span>
<a href="https://github.com/Shik3i/KoalaSync/issues" target="_blank" style="color: var(--accent); text-decoration: none;">GitHub Issues</a>
</p>
<p lang="en" style="margin-top: 0.5rem;">
<span style="opacity: 0.6;">🐛 Active concerns / Bug reports:</span>
<a href="https://github.com/Shik3i/KoalaSync/issues" target="_blank" style="color: var(--accent); text-decoration: none;">GitHub Issues</a>
</p>
</section>
<section style="opacity: 0.8;">
<h2>
<span lang="de">Privatprojekt-Hinweis</span>
<span lang="en">Private Project Notice</span>
</h2>
<p lang="de">
Diese Website ist ein rein privates Hobby-Projekt und dient keinen geschäftsmäßigen Zwecken.
Eine Impressumspflicht nach § 5 DDG (ehemals TMG) besteht daher nicht.
Diese Angaben erfolgen rein freiwillig zur Transparenz gegenüber der Community.
</p>
<p lang="en">
This website is a purely private hobby project and does not serve any commercial purposes.
Therefore, there is no obligation to provide a legal notice according to § 5 DDG (formerly TMG).
This information is provided voluntarily for transparency towards the community.
</p>
</section>
<section>
<h2>
<span lang="de">Haftung für Inhalte</span>
<span lang="en">Liability for Content</span>
</h2>
<p lang="de">
Gemäß § 7 Abs.1 DDG sind wir für eigene Inhalte verantwortlich. Nach §§ 8 bis 10 DDG sind wir jedoch nicht verpflichtet,
übermittelte oder gespeicherte fremde Informationen zu überwachen.
</p>
<p lang="en">
According to § 7 Abs.1 DDG we are responsible for our own content. According to §§ 8 to 10 DDG, however, we are not obligated to monitor transmitted or stored third-party information.
</p>
</section>
<section>
<h2>
<span lang="de">Haftung für Links</span>
<span lang="en">Liability for Links</span>
</h2>
<p lang="de">
Unser Angebot enthält Links zu externen Websites Dritter. Auf deren Inhalte haben wir keinen Einfluss und
können daher keine Gewähr für diese fremden Inhalte übernehmen.
</p>
<p lang="en">
Our offer contains links to external third-party websites. We have no influence on their content and therefore cannot assume any liability for these external contents.
</p>
</section>
<section>
<h2>
<span lang="de">Urheberrecht</span>
<span lang="en">Copyright</span>
</h2>
<p lang="de">
Die durch die Seitenbetreiber erstellten Inhalte auf diesen Seiten unterliegen dem deutschen Urheberrecht.
Vervielfältigung, Bearbeitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung.
</p>
<p lang="en">
The content and works created by the site operators on these pages are subject to German copyright law.
Duplication, processing, and any kind of exploitation outside the limits of copyright require written consent.
</p>
</section>
</div>
</main>
<footer>
<div class="container">
<p>&copy; 2026 KoalaSync. Open source under the MIT License.</p>
<p lang="de" style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
<p lang="en" style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="impressum" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Impressum</span><span lang="en">Legal Notice</span></a>
<a href="datenschutz" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Datenschutz</span><span lang="en">Privacy Policy</span></a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
Mastodon
</a>
</div>
</div>
</footer>
<script src="app.min.js"></script>
<p>Redirecting / Weiterleitung...</p>
</body>
</html>
+154
View File
@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="en" class="lang-en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Legal Notice | KoalaSync</title>
<link rel="preload" href="style.min.css" as="style">
<link rel="stylesheet" href="style.min.css">
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
<meta name="robots" content="noindex">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://sync.koalastuff.net/" },
{ "@type": "ListItem", "position": 2, "name": "Legal Notice", "item": "https://sync.koalastuff.net/imprint" }
]
}
</script>
<!-- Mobile Browser Theme Styling -->
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<script src="lang-init.min.js"></script>
</head>
<body>
<div class="bg-blobs">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<nav>
<div class="container nav-content">
<a href="./" class="logo-area" style="text-decoration: none;">
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span>
</a>
<div class="nav-links">
<a href="./" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width: 16px; height: 16px; display: block;"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<span>Home</span>
</a>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
GitHub
</a>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="globe-icon"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
<option value="en">🇬🇧 English</option>
<option value="de">🇩🇪 Deutsch</option>
<option value="fr">🇫🇷 Français</option>
<option value="es">🇪🇸 Español</option>
<option value="pt-BR">🇧🇷 Português (Brasil)</option>
<option value="ru">🇷🇺 Русский</option>
<option value="it">🇮🇹 Italiano</option>
<option value="pl">🇵🇱 Polski</option>
<option value="tr">🇹🇷 Türkçe</option>
<option value="nl">🇳🇱 Nederlands</option>
<option value="ja">🇯🇵 日本語</option>
<option value="ko">🇰🇷 한국어</option>
<option value="pt">🇵🇹 Português (Portugal)</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
</div>
</div>
</nav>
<main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;">
<img src="assets/KoalaImprintl.webp" alt="Cute Koala representing the legal notice page" class="legal-mascot" width="180" height="180">
</div>
<h1>Legal Notice</h1>
<p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
Transparency & Identification
</p>
<section>
<h2>Operator & Contact</h2>
<p>Timo (KoalaDev) Private Individual</p>
<p style="margin-top: 0.5rem;">
E-Mail: <span class="email-reveal" data-user="koalasync_admin" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[Show Email]</span>
</p>
<p style="margin-top: 0.75rem;">
<span style="opacity: 0.6;">🔒 Private message:</span>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--accent); text-decoration: none;">@koalastuff on Mastodon</a>
</p>
<p style="margin-top: 0.5rem;">
<span style="opacity: 0.6;">🐛 Active concerns / Bug reports:</span>
<a href="https://github.com/Shik3i/KoalaSync/issues" target="_blank" style="color: var(--accent); text-decoration: none;">GitHub Issues</a>
</p>
</section>
<section style="opacity: 0.8;">
<h2>Private Project Notice</h2>
<p>
This website is a purely private hobby project and does not serve any commercial purposes.
Therefore, there is no obligation to provide a legal notice according to § 5 DDG (formerly TMG).
This information is provided voluntarily for transparency towards the community.
</p>
</section>
<section>
<h2>Liability for Content</h2>
<p>
According to § 7 Abs.1 DDG we are responsible for our own content. According to §§ 8 to 10 DDG, however, we are not obligated to monitor transmitted or stored third-party information.
</p>
</section>
<section>
<h2>Liability for Links</h2>
<p>
Our offer contains links to external third-party websites. We have no influence on their content and therefore cannot assume any liability for these external contents.
</p>
</section>
<section>
<h2>Copyright</h2>
<p>
The content and works created by the site operators on these pages are subject to German copyright law.
Duplication, processing, and any kind of exploitation outside the limits of copyright require written consent.
</p>
</section>
</div>
</main>
<footer>
<div class="container">
<p>&copy; 2026 KoalaSync. Open source under the MIT License.</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="imprint" style="color: var(--text-muted); text-decoration: none;">Legal Notice</a>
<a href="privacy" style="color: var(--text-muted); text-decoration: none;">Privacy Policy</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
Mastodon
</a>
</div>
</div>
</footer>
<script src="app.min.js"></script>
</body>
</html>
+16 -7
View File
@@ -44,7 +44,6 @@
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="64" height="64">
<span>KoalaSync</span>
</a>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="./" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width: 16px; height: 16px; display: block;"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
@@ -54,18 +53,28 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
GitHub
</a>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="globe-icon"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="ru">Русский</option>
<option value="en">🇬🇧 English</option>
<option value="de">🇩🇪 Deutsch</option>
<option value="fr">🇫🇷 Français</option>
<option value="es">🇪🇸 Español</option>
<option value="pt-BR">🇧🇷 Português (Brasil)</option>
<option value="ru">🇷🇺 Русский</option>
<option value="it">🇮🇹 Italiano</option>
<option value="pl">🇵🇱 Polski</option>
<option value="tr">🇹🇷 Türkçe</option>
<option value="nl">🇳🇱 Nederlands</option>
<option value="ja">🇯🇵 日本語</option>
<option value="ko">🇰🇷 한국어</option>
<option value="pt">🇵🇹 Português (Portugal)</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
</div>
</div>
</nav>
+40 -7
View File
@@ -2,21 +2,51 @@
var html = document.documentElement;
var path = window.location.pathname;
var safeGetLocalStorage = function(key) {
try {
return localStorage.getItem(key);
} catch (_) {
return null;
}
};
var safeSetLocalStorage = function(key, val) {
try {
localStorage.setItem(key, val);
} catch (_) {
return;
}
};
// Mapping of browser language codes to KoalaSync locale directories
var langMap = {
'de': 'de',
'fr': 'fr',
'es': 'es',
'pt': 'pt-BR',
'ru': 'ru'
'pt-br': 'pt-BR',
'pt': 'pt',
'ru': 'ru',
'it': 'it',
'pl': 'pl',
'tr': 'tr',
'nl': 'nl',
'ja': 'ja',
'ko': 'ko'
};
var getBrowserLang = function() {
var fullLang = (navigator.language || '').toLowerCase();
if (fullLang.indexOf('pt-br') === 0) return 'pt-br';
if (fullLang.indexOf('pt') === 0) return 'pt';
return fullLang.split('-')[0];
};
// Check if we are on the root index page (either "/" or "/index.html" at the root)
var isRootIndex = path === '/' || path === '/index.html' || path === '';
if (isRootIndex) {
var savedLang = localStorage.getItem('koala_lang');
var browserLang = navigator.language.split('-')[0];
var savedLang = safeGetLocalStorage('koala_lang');
var browserLang = getBrowserLang();
var preferredLang = savedLang || langMap[browserLang] || 'en';
if (preferredLang !== 'en') {
@@ -38,10 +68,13 @@
}
if (hasStaticLang) {
localStorage.setItem('koala_lang', activeLang);
var isLegal = path.indexOf('impressum') !== -1 || path.indexOf('datenschutz') !== -1 || path.indexOf('imprint') !== -1 || path.indexOf('privacy') !== -1;
if (!isLegal) {
safeSetLocalStorage('koala_lang', activeLang);
}
} else {
var savedLang = localStorage.getItem('koala_lang');
var browserLang = navigator.language.split('-')[0];
var savedLang = safeGetLocalStorage('koala_lang');
var browserLang = getBrowserLang();
activeLang = savedLang || langMap[browserLang] || 'en';
// Dynamic utility pages currently only support English and German markup.
+7 -1
View File
@@ -69,6 +69,10 @@
"COMP_FEAT_5_DESC": "Unterstützte Webseiten und Player-Kompatibilität.",
"COMP_FEAT_5_KOALA": "Fast jedes HTML5-Video",
"COMP_FEAT_5_TELE": "Nur unterstützte Seiten",
"COMP_FEAT_6_NAME": "Lokalisierung / Übersetzung",
"COMP_FEAT_6_DESC": "Verfügbare Oberflächensprachen und Übersetzungsunterstützung.",
"COMP_FEAT_6_KOALA": "6 Sprachen (wachsend)",
"COMP_FEAT_6_TELE": "Nur Englisch",
"COMP_FOOTNOTE_1": "Stand des Vergleichs: Mai 2026.",
"COMP_FOOTNOTE_2": "Details zu Preisen und unterstützten Netzwerken von Teleparty Premium:",
"COMP_FOOTNOTE_3": "Datenschutzerklärung und Tracker-Erfassung von Teleparty:",
@@ -157,5 +161,7 @@
"MOCK_29": "Video-Debug-Info",
"MOCK_30": "Aktivitätsverlauf",
"MOCK_31": "Protokoll (Letzte 50)",
"MOCK_32": "LÖSCHEN"
"MOCK_32": "LÖSCHEN",
"FOOTER_LEGAL_LINK": "de/impressum",
"FOOTER_PRIVACY_LINK": "de/datenschutz"
}
+7 -1
View File
@@ -69,6 +69,10 @@
"COMP_FEAT_5_DESC": "Supported websites and player compatibility.",
"COMP_FEAT_5_KOALA": "Almost any HTML5 Video",
"COMP_FEAT_5_TELE": "Supported sites only",
"COMP_FEAT_6_NAME": "Localization / Translation",
"COMP_FEAT_6_DESC": "Available interface languages and translation support.",
"COMP_FEAT_6_KOALA": "6 Languages (and growing)",
"COMP_FEAT_6_TELE": "English only",
"COMP_FOOTNOTE_1": "Comparison state: May 2026.",
"COMP_FOOTNOTE_2": "Official Teleparty Premium pricing and supported networks details:",
"COMP_FOOTNOTE_3": "Teleparty privacy policies and tracking data collection:",
@@ -157,5 +161,7 @@
"MOCK_29": "Video Debug Info",
"MOCK_30": "Full Action History",
"MOCK_31": "Logs (Last 50)",
"MOCK_32": "CLEAR"
"MOCK_32": "CLEAR",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
}
+7 -1
View File
@@ -69,6 +69,10 @@
"COMP_FEAT_5_DESC": "Sitios web compatibles y compatibilidad de reproductores.",
"COMP_FEAT_5_KOALA": "Casi cualquier video HTML5",
"COMP_FEAT_5_TELE": "Solo sitios compatibles",
"COMP_FEAT_6_NAME": "Localización / Traducción",
"COMP_FEAT_6_DESC": "Idiomas de interfaz disponibles y soporte de traducción.",
"COMP_FEAT_6_KOALA": "6 idiomas (y creciendo)",
"COMP_FEAT_6_TELE": "Solo inglés",
"COMP_FOOTNOTE_1": "Estado de la comparación: mayo de 2026.",
"COMP_FOOTNOTE_2": "Detalles oficiales de precios de Teleparty Premium y redes compatibles:",
"COMP_FOOTNOTE_3": "Políticas de privacidad y recopilación de datos de seguimiento de Teleparty:",
@@ -157,5 +161,7 @@
"MOCK_29": "Historial completo",
"MOCK_30": "Registros (últimos 50)",
"MOCK_31": "BORRAR",
"MOCK_32": "CLEAR"
"MOCK_32": "CLEAR",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
}
+7 -1
View File
@@ -69,6 +69,10 @@
"COMP_FEAT_5_DESC": "Sites web pris en charge et compatibilité des lecteurs.",
"COMP_FEAT_5_KOALA": "Presque toutes les vidéos HTML5",
"COMP_FEAT_5_TELE": "Sites pris en charge uniquement",
"COMP_FEAT_6_NAME": "Localisation / Traduction",
"COMP_FEAT_6_DESC": "Langues d'interface disponibles et support de traduction.",
"COMP_FEAT_6_KOALA": "6 langues (et plus à venir)",
"COMP_FEAT_6_TELE": "Anglais uniquement",
"COMP_FOOTNOTE_1": "État de la comparaison : mai 2026.",
"COMP_FOOTNOTE_2": "Détails sur les tarifs officiels de Teleparty Premium et les réseaux pris en charge :",
"COMP_FOOTNOTE_3": "Politiques de confidentialité et collecte de données de suivi de Teleparty :",
@@ -157,5 +161,7 @@
"MOCK_29": "Historique complet d'actions",
"MOCK_30": "Logs (50 derniers)",
"MOCK_31": "EFFACER",
"MOCK_32": "CLEAR"
"MOCK_32": "CLEAR",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
}
+167
View File
@@ -0,0 +1,167 @@
{
"LANG_CODE": "it",
"HTML_CLASS": "lang-it",
"CANONICAL_PATH": "it/",
"LANG_TOGGLE_URL": "",
"LANG_TOGGLE_TEXT": "EN",
"OG_LOCALE": "it_IT",
"META_TITLE": "KoalaSync | Sincronizza Netflix, YouTube e qualsiasi video con gli amici",
"META_DESCRIPTION": "Guarda Netflix, YouTube, Twitch e qualsiasi video HTML5 in perfetta sincronia con gli amici. Estensione del browser gratuita e open source per Chrome e Firefox. Nessuna registrazione richiesta.",
"OG_TITLE": "KoalaSync | Sincronizza Netflix, Emby, Jellyfin e quasi tutti i video con gli amici",
"OG_DESCRIPTION": "Guarda Netflix, Emby, Jellyfin, YouTube, Twitch e quasi tutti i video HTML5 in perfetta sincronia. Estensione del browser open source e attenta alla privacy per Chrome e Firefox.",
"TWITTER_TITLE": "KoalaSync | Sincronizza Netflix, Emby, Jellyfin e quasi tutti i video con gli amici Estensione Browser",
"TWITTER_DESCRIPTION": "Guarda Netflix, Emby, Jellyfin, YouTube, Twitch e quasi tutti i video HTML5 in perfetta sincronia con gli amici. Estensione del browser open source e attenta alla privacy per Chrome e Firefox.",
"NAV_FEATURES": "Funzionalità",
"NAV_HOW_IT_WORKS": "Come Funziona",
"HERO_TITLE": "Guarda Insieme.<br>Sincronizza Perfettamente.",
"HERO_SUBTITLE": "La tua serata cinema a distanza senza lag. Nessuna registrazione, nessun dato raccolto. Condividi un link e guarda insieme.",
"HERO_MASCOT_ALT": "Un simpatico koala in piedi che guarda in basso verso i pulsanti di download",
"ADD_TO_CHROME": "Aggiungi a Chrome",
"ADD_TO_FIREFOX": "Aggiungi a Firefox",
"COMPAT_HEADING": "Funziona sulle tue piattaforme preferite",
"COMPAT_MORE": "e molti altri",
"COMPAT_TOOLTIP": "Funziona su quasi tutti i siti con un elemento video",
"USE_CASES_TITLE": "Perfetto per Ogni Occasione",
"USE_CASES_SUBTITLE": "Vicini o lontani, KoalaSync unisce le persone attorno ai loro video preferiti.",
"USE_CASE_1_ALT": "Due simpatici koala seduti insieme che condividono un secchio di popcorn",
"USE_CASE_1_TITLE": "Serata Cinema con gli Amici",
"USE_CASE_1_DESC": "Sincronizza i tuoi film in tempo reale e parla via Discord, Zoom o la tua app di chiamata vocale preferita. Sembrerà di essere nella stessa stanza.",
"USE_CASE_2_ALT": "Un professore koala che presenta su uno schermo a due studenti koala a distanza",
"USE_CASE_2_TITLE": "Apprendimento a Distanza",
"USE_CASE_2_DESC": "Analizza tutorial online, lezioni o stream di formazione per sviluppatori insieme a compagni di classe o colleghi. Fai pausa e discuti i passaggi complessi in perfetta armonia.",
"USE_CASE_3_ALT": "Una simpatica coppia di koala che guarda insieme a distanza; uno è seduto al PC e l'altro è a letto con un laptop, con cuori che volano tra loro",
"USE_CASE_3_TITLE": "Relazioni a Distanza",
"USE_CASE_3_DESC": "Supera le distanze e goditi le serate romantiche. Vivi ogni colpo di scena, ridi alle stesse battute e condividi momenti emozionanti nello stesso identico millisecondo.",
"WHY_TITLE": "Perché KoalaSync?",
"WHY_SUBTITLE": "Progettato per una sincronizzazione affidabile, privacy al primo posto e configurazione semplice.",
"FEATURE_1_TITLE": "Controllo Totale / Sincronizzazione Istantanea",
"FEATURE_1_DESC": "Uno mette in pausa, tutti mettono in pausa. Uno cerca, tutti lo seguono. Il nostro protocollo di sincronizzazione a due fasi coordina la riproduzione in tempo reale tra tutti i partecipanti.",
"FEATURE_2_TITLE": "Binge-Watching Infinito",
"FEATURE_2_DESC": "Riproduzione automatica sincronizzata. KoalaSync rileva automaticamente i cambi di episodio e blocca la riproduzione finché tutti i partecipanti non hanno caricato il video successivo.",
"FEATURE_3_TITLE": "Zero Account / Massima Privacy",
"FEATURE_3_DESC": "Nessuna registrazione, nessun tracciamento, nessuna traccia persistente. Il server funziona interamente nella RAM effimera e cancella la tua stanza quando te ne vai.",
"FEATURE_4_TITLE": "Supporto HTML5 Universale",
"FEATURE_4_DESC": "Funziona su YouTube, Twitch, Netflix, Jellyfin, Emby e quasi tutte le pagine web standard contenenti un elemento video HTML5. Compatibile anche con server personalizzati.",
"FEATURE_5_TITLE": "Ospitabile Autonomamente e Pronto per Docker",
"FEATURE_5_DESC": "I nostri server ufficiali sono gratuiti e veloci, ma puoi gestirne uno tuo se preferisci. Avvia il tuo server di inoltro privato in pochi secondi tramite Docker.",
"FEATURE_6_TITLE": "Inviti Istantanei / Accesso con 1 Click",
"FEATURE_6_DESC": "Nessun indirizzo IP o password da scambiare. Condividi un link di invito generato con i tuoi amici per farli entrare nella stanza automaticamente con un solo clic.",
"COMP_TITLE": "KoalaSync vs. Teleparty",
"COMP_SUBTITLE": "Scopri perché l'open source, senza pubblicità e attento alla privacy è il modo migliore per guardare i video insieme.",
"COMP_COL_FEATURE": "Funzionalità",
"COMP_FEAT_1_NAME": "Costi / Paywall",
"COMP_FEAT_1_DESC": "Abbonamenti premium, costi nascosti o funzionalità bloccate.",
"COMP_FEAT_1_KOALA": "100% Gratuito",
"COMP_FEAT_1_TELE": "Piani a Pagamento / Blocchi Premium",
"COMP_FEAT_2_NAME": "Licenza / Codice Controllabile",
"COMP_FEAT_2_DESC": "Indica se il codice sorgente è aperto e liberamente controllabile.",
"COMP_FEAT_2_KOALA": "Open Source (MIT)",
"COMP_FEAT_2_TELE": "Proprietario",
"COMP_FEAT_3_NAME": "Ospitare in Proprio",
"COMP_FEAT_3_DESC": "Possibilità di distribuire il proprio server di inoltro privato.",
"COMP_FEAT_3_KOALA": "Sì (Pronto per Docker)",
"COMP_FEAT_3_TELE": "No",
"COMP_FEAT_4_NAME": "Privacy e Tracciamento",
"COMP_FEAT_4_DESC": "Requisiti di registrazione, cookie di tracciamento e analisi.",
"COMP_FEAT_4_KOALA": "Nessuna Persistenza (Solo RAM)",
"COMP_FEAT_4_TELE": "Google Analytics e Cookie",
"COMP_FEAT_5_NAME": "Compatibilità dei Siti",
"COMP_FEAT_5_DESC": "Siti web supportati e compatibilità dei player.",
"COMP_FEAT_5_KOALA": "Quasi tutti i video HTML5",
"COMP_FEAT_5_TELE": "Solo siti supportati",
"COMP_FEAT_6_NAME": "Localizzazione / Traduzione",
"COMP_FEAT_6_DESC": "Lingue dell'interfaccia disponibili e supporto alle traduzioni.",
"COMP_FEAT_6_KOALA": "13 Lingue (in crescita)",
"COMP_FEAT_6_TELE": "Solo Inglese",
"COMP_FOOTNOTE_1": "Stato del confronto: Maggio 2026.",
"COMP_FOOTNOTE_2": "Dettagli sui prezzi di Teleparty Premium e reti supportate:",
"COMP_FOOTNOTE_3": "Informativa sulla privacy di Teleparty e raccolta dei dati di tracciamento:",
"COMP_FOOTNOTE_4": "Funziona sui siti web che consentono l'iniezione di script nei tag video HTML5 standard. I siti web con Content Security Policy (CSP) molto rigide, protezioni DRM o wrapper del player fortemente offuscati (come shadow DOM complessi) potrebbero limitare il controllo o l'iniezione.",
"STEPS_TITLE": "Guida Introduttiva",
"STEP_1_TITLE": "Installa l'Estensione",
"STEP_1_DESC": "Aggiungi KoalaSync al tuo browser dal Chrome Web Store, da Firefox Add-ons, o scarica l'ultimo ZIP per sviluppatori da GitHub.",
"STEP_1_ILLUS_DESC": "Sincronizzatore video attento alla privacy",
"STEP_1_ILLUS_DL_CHROME": "Scarica per Chrome",
"STEP_1_ILLUS_DL_FIREFOX": "Scarica per Firefox",
"STEP_1_ILLUS_ACTIVE": "Estensione Attiva",
"STEP_1_ILLUS_READY": "Pronti a guardare insieme!",
"STEP_2_TITLE": "Crea una Stanza",
"STEP_2_DESC": "Apri il popup dell'estensione e fai clic su '+ Crea Nuova Stanza'. KoalaSync genererà automaticamente un ID Stanza e una Password sicuri, entrerà nella stanza e copierà il link di invito negli appunti.",
"STEP_2_ILLUS_ROOM": "Stanza",
"STEP_2_ILLUS_SYNC": "Sincronizza",
"STEP_2_ILLUS_SETTINGS": "Impostazioni",
"STEP_2_ILLUS_CREATE": "+ Crea Nuova Stanza",
"STEP_2_ILLUS_MANUAL": "Connessione Manuale / Avanzata",
"STEP_2_ILLUS_COPIED": "Link di invito copiato!",
"STEP_3_TITLE": "Condividi e Sincronizza",
"STEP_3_DESC": "Invia il link di invito ai tuoi amici. Una volta entrati, seleziona la scheda del tuo video e goditi la riproduzione sincronizzata.",
"STEP_3_ILLUS_IN_SYNC": "IN SINCRONIA",
"SELF_TITLE": "Per chi Ospita Autonomamente",
"SELF_SUBTITLE": "Non ti fidi del nostro server? Mantieni la piena sovranità sui tuoi dati. Distribuisci il tuo server di inoltro privato in pochi minuti.",
"SELF_MASCOT_ALT": "Un simpatico koala seduto al laptop che distribuisce un contenitore Docker per l'hosting autonomo",
"SELF_COPY_CODE": "Copia Codice",
"SELF_GITHUB_PACKAGES": "Visualizza tutti i tag delle immagini su GitHub Packages",
"BOTTOM_TITLE": "Non sei convinto? Guarda tu stesso.",
"BOTTOM_SUBTITLE": "KoalaSync è al 100% open source, senza pubblicità e senza tracciamento. Controlla il nostro codice su GitHub o installa direttamente l'estensione del browser.",
"BOTTOM_MASCOT_ALT": "Un simpatico koala che tiene una pagina del repository GitHub vicino alla mascotte di GitHub Octocat",
"FAQ_TITLE": "Domande Frequenti",
"FAQ_SUBTITLE": "Tutto quello che c'è da sapere sulla visione dei video insieme in perfetta sincronia.",
"FAQ_Q1": "Posso guardare Netflix, Emby o Jellyfin in sincronia con gli amici?",
"FAQ_A1": "Sì! KoalaSync funziona su Netflix, YouTube, Twitch e qualsiasi sito web con un lettore video HTML5 — inclusi Emby, Jellyfin e server multimediali personalizzati. Installa l'estensione, crea una stanza, condividi il link di invito e tutti guarderanno in perfetta sincronia.",
"FAQ_Q2": "KoalaSync è un'alternativa gratuita a Teleparty?",
"FAQ_A2": "Gratuito al 100% e open source con licenza MIT. Nessun piano premium, nessuna funzionalità bloccata, nessuna pubblicità — tutto è incluso. A differenza di Teleparty, il server di inoltro di KoalaSync è solo in RAM, con zero raccolta dati o account utente richiesti.",
"FAQ_Q3": "KoalaSync funziona con Emby, Jellyfin e server multimediali ospitati in proprio?",
"FAQ_A3": "Sì. KoalaSync rileva qualsiasi elemento video HTML5 standard, il che significa che funziona perfettamente con Emby, Jellyfin, Plex e qualsiasi configurazione di streaming personalizzata. Puoi anche ospitare autonomamente il server di inoltro tramite Docker.",
"FAQ_Q4": "Quali browser e piattaforme supportano KoalaSync?",
"FAQ_A4": "Google Chrome, Mozilla Firefox, Microsoft Edge, Opera, Brave e tutti i browser basati su Chromium. Installalo direttamente dal Chrome Web Store o da Firefox Add-ons — non è richiesta alcuna configurazione manuale.",
"FAQ_Q5": "Tutti i miei amici devono avere l'estensione installata?",
"FAQ_A5": "Sì, ogni partecipante deve avere KoalaSync installato — ma non è richiesta alcuna registrazione o creazione di account. Fai clic sul link di invito, installa l'estensione se non l'hai già fatto ed entrerai automaticamente nella stanza in pochi secondi.",
"FAQ_Q6": "I miei dati sono al sicuro? E se non mi fido del server ufficiale?",
"FAQ_A6": "Completamente al sicuro. Il server di inoltro di KoalaSync è solo in RAM — tutti i dati della sessione vengono eliminati permanentemente nel momento in cui lasci la stanza. Nessun database, nessun log, nessuna analisi, nessun account. E se preferisci il controllo totale: puoi ospitare autonomamente il tuo server di inoltro privato tramite Docker in pochi minuti. L'intera codebase è open source e controllabile su GitHub.",
"HOWTO_TITLE": "Come sincronizzare i video con gli amici usando KoalaSync",
"HOWTO_DESC": "Guarda qualsiasi video in perfetta sincronia con gli amici su YouTube, Netflix, Twitch e altri. Nessuna registrazione, nessun tracciamento.",
"HOWTO_STEP_1_NAME": "Installa l'estensione del browser",
"HOWTO_STEP_1_TEXT": "Aggiungi KoalaSync al tuo browser dal Chrome Web Store o da Firefox Add-ons. Funziona su qualsiasi sito web con un elemento video.",
"HOWTO_STEP_2_NAME": "Crea una stanza",
"HOWTO_STEP_2_TEXT": "Apri il popup dell'estensione e fai clic su Crea Nuova Stanza. Il link di invito viene copiato automaticamente negli appunti.",
"HOWTO_STEP_3_NAME": "Condividi e sincronizza",
"HOWTO_STEP_3_TEXT": "Invia il link di invito ai tuoi amici. Scegli una scheda video e goditi la riproduzione sincronizzata in tempo reale.",
"FOOTER_MIT": "Open source con licenza MIT.",
"FOOTER_RAM": "Nessun dato viene memorizzato sui nostri server. Inoltro basato esclusivamente su RAM.",
"FOOTER_LEGAL": "Note Legali",
"FOOTER_PRIVACY": "Informativa sulla Privacy",
"MOCK_01": "Stanza Attiva",
"MOCK_02": "Server Ufficiale",
"MOCK_03": "Link di Invito",
"MOCK_04": "Partecipanti nella Stanza",
"MOCK_05": "YOU",
"MOCK_06": "Playing",
"MOCK_07": "Peer",
"MOCK_08": "Lascia Stanza",
"MOCK_09": "Seleziona Video",
"MOCK_10": "Controllo Remoto",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Play",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pause",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SYNC",
"MOCK_14": "Salto dagli Altri",
"MOCK_15": "Stato Ultima Attività",
"MOCK_16": "In attesa di 1 partecipante (KoalaPC)...",
"MOCK_17": "Salta e riproduci comunque",
"MOCK_18": "Tuo Nome Utente",
"MOCK_19": "Nascondi Schede Inutili",
"MOCK_20": "Auto-Sync Prossimo Episodio",
"MOCK_21": "Copia Automatica Invito",
"MOCK_22": "Notifiche del Browser",
"MOCK_23": "Risoluzione Problemi",
"MOCK_24": "Rigenera ID Peer",
"MOCK_25": "Usa questo se riscontri errori di \"Identità Duplicata\".",
"MOCK_26": "Stato Connessione",
"MOCK_27": "Connected",
"MOCK_28": "Copia Log",
"MOCK_29": "Info Debug Video",
"MOCK_30": "Cronologia Azioni Completa",
"MOCK_31": "Log (Ultimi 50)",
"MOCK_32": "PULISCI",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
}

Some files were not shown because too many files have changed in this diff Show More