feat(extension): implement full dynamic i18n support and automated consistency checks

This commit is contained in:
Koala
2026-06-01 01:05:46 +02:00
parent 3fcafbd081
commit fd47eb82b9
12 changed files with 1579 additions and 118 deletions
+22 -8
View File
@@ -1,5 +1,7 @@
import { EVENTS, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, APP_VERSION, EPISODE_LOBBY_TIMEOUT, FORCE_SYNC_TIMEOUT } from './shared/constants.js';
import { generateUsername } from './shared/names.js';
import { loadLocale, getMessage } from './i18n.js';
// --- State Management ---
let socket = null;
@@ -471,26 +473,38 @@ function updateBadgeStatus() {
}
function showNotification(senderName, action) {
chrome.storage.sync.get(['browserNotifications'], (settings) => {
chrome.storage.sync.get(['browserNotifications', 'locale'], async (settings) => {
if (!settings.browserNotifications) return;
const label = action === 'play' ? 'started playback' :
action === 'pause' ? 'paused playback' :
action === 'seek' ? 'seeked the video' :
action === 'force_sync_prepare' ? 'started force sync' :
action === 'force_sync_execute' ? 'synchronized everyone' : action;
const lang = settings.locale || 'en';
await loadLocale(lang);
let labelKey = '';
if (action === 'play') labelKey = 'NOTIF_PLAY';
else if (action === 'pause') labelKey = 'NOTIF_PAUSE';
else if (action === 'seek') labelKey = 'NOTIF_SEEK';
else if (action === 'force_sync_prepare') labelKey = 'NOTIF_FORCE_PREPARE';
else if (action === 'force_sync_execute') labelKey = 'NOTIF_FORCE_EXECUTE';
const label = labelKey ? getMessage(labelKey) : action;
let displayName = senderName || 'A peer';
if (currentRoom && Array.isArray(currentRoom.peers)) {
const peer = currentRoom.peers.find(p => (p.peerId || p) === senderName);
if (peer && peer.username) displayName = peer.username;
}
if (displayName === 'You' || displayName === 'YOU') {
displayName = getMessage('LABEL_YOU') || 'YOU';
}
const message = getMessage('TOAST_PEER_ACTION', { name: displayName, action: label }) + '.';
chrome.notifications.create(`sync_${Date.now()}`, {
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'KoalaSync',
message: `${displayName} ${label}.`,
message: message,
priority: 1
});
});
+89
View File
@@ -0,0 +1,89 @@
// extension/i18n.js
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru'];
export const DEFAULT_LANGUAGE = 'en';
let activeDictionary = {};
/**
* Resolves, loads, and merges the target language with the English baseline fallback.
* @param {string} langCode - Target language (e.g. 'de')
*/
export async function loadLocale(langCode) {
const resolvedLang = SUPPORTED_LANGUAGES.includes(langCode) ? langCode : DEFAULT_LANGUAGE;
try {
// Load Baseline English
const enResponse = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
const enDict = await enResponse.json();
if (resolvedLang === DEFAULT_LANGUAGE) {
activeDictionary = enDict;
return;
}
// Load Target Locale
const targetResponse = await fetch(chrome.runtime.getURL(`locales/${resolvedLang}.json`));
const targetDict = await targetResponse.json();
// Airtight Fallback Merge: target overrides en, missing elements fallback to en
activeDictionary = Object.assign({}, enDict, targetDict);
} 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();
} catch (_) {
activeDictionary = {};
}
}
}
/**
* Returns the translated string for a given key. Supports optional value interpolation.
* @param {string} key - Dictionary key
* @param {object} [placeholders] - Key-value map for replacements (e.g., { name: 'Alice' })
* @returns {string} Translated string or the key itself
*/
export function getMessage(key, placeholders = null) {
let msg = activeDictionary[key] || key;
if (placeholders && typeof placeholders === 'object') {
for (const [k, v] of Object.entries(placeholders)) {
msg = msg.replace(new RegExp(`{${k}}`, 'g'), v);
}
}
return msg;
}
/**
* Performs dynamic DOM replacements for elements carrying data-i18n attributes.
*/
export function translateDOM() {
// 1. Text Content
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
const translated = getMessage(key);
// Special case: Preserve logo image elements inside headers (like h1 logo)
const img = el.querySelector('img');
if (img) {
el.innerHTML = '';
el.appendChild(img);
el.appendChild(document.createTextNode(' ' + translated));
} else {
el.textContent = translated;
}
});
// 2. Tooltips (titles)
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
el.setAttribute('title', getMessage(key));
});
// 3. Placeholders
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
el.setAttribute('placeholder', getMessage(key));
});
}
+177
View File
@@ -0,0 +1,177 @@
{
"LANG_CODE": "de",
"HTML_CLASS": "lang-de",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Raum",
"TAB_ROOM_TOOLTIP": "Raum-Einstellungen und Verbindung",
"TAB_SYNC": "Sync",
"TAB_SYNC_TOOLTIP": "Video-Synchronisations-Steuerung und Aktionen",
"TAB_SETTINGS": "Optionen",
"TAB_SETTINGS_TOOLTIP": "Erweiterungseinstellungen",
"TAB_STATUS": "Status",
"TAB_STATUS_TOOLTIP": "Erweiterte Diagnose & Protokolle",
"BTN_CREATE_ROOM": "+ Neuer Raum",
"MANUAL_CONNECT_HEADER": "Manuell verbinden / Erweitert",
"LABEL_SERVER": "Server",
"BTN_SERVER_OFFICIAL": "Offiziell",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Verwende den offiziellen, zuverlässigen Server",
"BTN_SERVER_CUSTOM": "Eigener",
"BTN_SERVER_CUSTOM_TOOLTIP": "Verbinde dich mit deinem eigenen, selbstgehosteten Server",
"PLACEHOLDER_SERVER_URL": "wss://dein-server:3000",
"LABEL_ROOM_ID": "Raum-ID",
"LABEL_ROOM_ID_TOOLTIP": "Der eindeutige Identifikator für deinen Sync-Raum",
"PLACEHOLDER_ROOM_ID": "Raum-ID eingeben",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "Die eindeutige ID des Raums, dem du beitreten möchtest",
"LABEL_PASSWORD": "Passwort (Optional)",
"LABEL_PASSWORD_TOOLTIP": "Optionales Passwort, um den Raumzugang zu beschränken",
"PLACEHOLDER_PASSWORD": "Raum-Passwort (optional)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Passwort für den Raum (leer lassen, wenn keines vorhanden)",
"BTN_JOIN_ROOM": "Raum beitreten",
"BTN_JOIN_ROOM_TOOLTIP": "Mit dem Raum verbinden",
"LABEL_PUBLIC_ROOMS": "Öffentliche Räume",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Liste der öffentlich verfügbaren Räume auf diesem Server",
"BTN_REFRESH": "AKTUALISIEREN",
"BTN_REFRESH_TOOLTIP": "Die Liste der öffentlichen Räume aktualisieren",
"PUBLIC_ROOMS_REFRESHING": "Aktualisiere...",
"LABEL_ACTIVE_ROOM": "Aktiver Raum",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Der Raum, mit dem du gerade verbunden bist",
"ACTIVE_ROOM_NONE": "KEINER",
"ACTIVE_SERVER_OFFICIAL": "Offizieller Server",
"LABEL_INVITE_LINK": "Einladungslink",
"LABEL_INVITE_LINK_TOOLTIP": "Teile diesen Link mit Freunden, damit sie beitreten können",
"LABEL_PEERS_IN_ROOM": "Teilnehmer im Raum",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Andere Benutzer, die derzeit mit diesem Raum verbunden sind",
"NO_PEERS_CONNECTED": "Keine Teilnehmer verbunden",
"BTN_LEAVE_ROOM": "Raum verlassen",
"LABEL_SELECT_VIDEO": "Video auswählen",
"LABEL_SELECT_VIDEO_TOOLTIP": "Wähle den Browser-Tab aus, der das zu synchronisierende Video enthält",
"OPTION_SELECT_TAB": "-- Wähle einen Tab --",
"LABEL_REMOTE_CONTROL": "Fernsteuerung",
"BTN_COPY_INVITE": "📋 Einladungslink",
"BTN_COPY_INVITE_TOOLTIP": "Einladungslink kopieren",
"BTN_PLAY": "▶ Abspielen",
"BTN_PLAY_TOOLTIP": "Sende einen Play-Befehl an alle",
"BTN_PAUSE": "⏸ Pause",
"BTN_PAUSE_TOOLTIP": "Sende einen Pause-Befehl an alle",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Erzwinge die Synchronisation aller Teilnehmer",
"OPTION_JUMP_TO_OTHERS": "Zu anderen springen",
"OPTION_JUMP_TO_ME": "Zu mir springen",
"OPTION_JUMP_MODE_TOOLTIP": "Synchronisationsziel auswählen",
"LABEL_LAST_ACTIVITY": "Letzter Aktivitätsstatus",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Zeigt den neuesten Play-, Pause- oder Seek-Befehl",
"NO_RECENT_COMMANDS": "Keine aktuellen Befehle",
"LOBBY_HEADER": "EPISODEN-LOBBY",
"LOBBY_WAITING_FOR": "🎬 Warte auf: \"{title}\"",
"LOBBY_WAITING_PEERS": "Warte auf Teilnehmer...",
"BTN_SKIP_PLAY": "Überspringen & Abspielen",
"BTN_SKIP_PLAY_TOOLTIP": "Lobby abbrechen und trotzdem abspielen",
"LOBBY_CONNECT_FIRST": "Zuerst Raum beitreten",
"LOBBY_CONNECT_FIRST_DESC": "Du musst über einen Einladungslink beitreten oder einen neuen Raum erstellen, um Videos zu synchronisieren.",
"BTN_CREATE_ROOM_ALT": "Neuen Raum erstellen",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Einen neuen zufälligen Raum erstellen und beitreten",
"LABEL_USERNAME": "Dein Benutzername",
"LABEL_USERNAME_TOOLTIP": "Der Benutzername hilft anderen, dich zu identifizieren.",
"PLACEHOLDER_USERNAME": "Anonymer Koala",
"LABEL_HIDE_CLUTTER": "Aufgeräumte Tab-Liste",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtert Nicht-Video-Tabs und irrelevante Domains heraus, um die Liste sauber zu halten",
"LABEL_AUTO_SYNC_NEXT": "Nächste Episode auto-syncen",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausiert automatisch und wartet auf alle Teilnehmer bei Episodenwechsel, startet dann synchron.",
"LABEL_AUTO_COPY_INVITE": "Einladungslink auto-kopieren",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Kopiert den Einladungslink automatisch beim Erstellen eines Raums.",
"LABEL_NOTIFICATIONS": "Browser-Benachrichtigungen",
"LABEL_NOTIFICATIONS_TOOLTIP": "Zeigt Systembenachrichtigungen, wenn jemand beitritt/verlässt oder abspielt/pausiert.",
"LABEL_LANGUAGE": "Sprache",
"LABEL_LANGUAGE_TOOLTIP": "Wähle deine bevorzugte Sprache für die Erweiterung aus",
"LABEL_TROUBLESHOOTING": "Fehlerbehebung",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Werkzeuge zur Behebung von Verbindungsproblemen",
"BTN_REGEN_ID": "Peer-ID neu generieren",
"BTN_REGEN_ID_TOOLTIP": "Generiere deine interne ID neu und verbinde dich erneut",
"REGEN_ID_DESC": "Verwende dies, wenn du Fehler wegen doppelter Identität siehst.",
"LABEL_CONN_STATUS": "Verbindungsstatus",
"LABEL_CONN_STATUS_TOOLTIP": "Aktueller WebSocket-Verbindungsstatus",
"CONN_STATUS_DISCONNECTED": "Getrennt",
"BTN_RETRY": "WIEDERHOLEN",
"BTN_RETRY_TOOLTIP": "Versuchen, die Verbindung zum Server wiederherzustellen",
"BTN_COPY_LOGS": "Logs kopieren",
"BTN_COPY_LOGS_TOOLTIP": "Logs zum Teilen in die Zwischenablage kopieren",
"LABEL_VIDEO_DEBUG": "Video-Debug-Info",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Technische Details zum aktuell ausgewählten Video-Element",
"VIDEO_DEBUG_EMPTY": "Kein Tab ausgewählt oder kein Video erkannt.",
"LABEL_HISTORY": "Aktionsverlauf",
"LABEL_HISTORY_TOOLTIP": "Chronologisches Protokoll aller Sync-Befehle im Raum",
"HISTORY_EMPTY": "Noch keine Aktivität",
"LABEL_LOGS": "Logs (Letzte 50)",
"LABEL_LOGS_TOOLTIP": "Technische Verbindungsprotokolle zur Fehlersuche",
"BTN_CLEAR": "LEEREN",
"BTN_CLEAR_TOOLTIP": "Protokollausgabe leeren",
"LABEL_GITHUB": "GitHub-Repository",
"BTN_ONBOARDING_SKIP": "Überspringen",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Einführung überspringen",
"BTN_ONBOARDING_NEXT": "Weiter",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Zum nächsten Schritt gehen",
"ONBOARDING_1_TITLE": "Willkommen bei KoalaSync!",
"ONBOARDING_1_TEXT": "Schau Videos in perfekter Synchronität zusammen — egal wo ihr seid. Machen wir eine kurze Tour!",
"ONBOARDING_2_TITLE": "1. Raum erstellen",
"ONBOARDING_2_TEXT": "Hier geht's los. Erstelle einen Raum und teile den Einladungslink mit deinen Freunden.",
"ONBOARDING_3_TITLE": "2. Video auswählen",
"ONBOARDING_3_TEXT": "Wähle hier das Video aus, das du synchronisieren willst. Abspielen, Pause, Spulen — alle bleiben synchron.",
"ONBOARDING_4_TITLE": "3. Personalisieren",
"ONBOARDING_4_TEXT": "Wähle einen lustigen Benutzernamen, damit deine Freunde wissen, wer du bist.",
"ONBOARDING_5_TITLE": "Alles startklar!",
"ONBOARDING_5_TEXT": "Zeit, das Popcorn zu holen. Viel Spaß beim gemeinsamen Schauen!",
"ERR_CONN_TIMEOUT": "Verbindung abgelaufen. Bitte versuche es erneut.",
"ERR_INVALID_SERVER_URL": "Ungültiges Server-URL-Format.",
"ERR_IDENTITY_NOT_LOADED": "Identität noch nicht geladen. Warte kurz und versuche es erneut.",
"ERR_NO_PEERS_TIME": "Keine anderen Teilnehmer mit bekannter Zeit. Wechsle zu 'Zu mir springen'.",
"ERR_NO_VIDEO_TAB": "Verbindung zum Video-Tab fehlgeschlagen.",
"ERR_SELECT_VIDEO": "Bitte wähle zuerst ein Video aus!",
"TOAST_INVITE_COPIED": "Einladungslink kopiert!",
"TOAST_COPY_FAILED": "Kopieren in Zwischenablage fehlgeschlagen",
"TOAST_LOBBY_SKIPPED": "Episoden-Lobby übersprungen.",
"TOAST_LOBBY_SKIP_FAILED": "Überspringen der Lobby fehlgeschlagen.",
"TOAST_LOGS_COPIED": "Kopiert!",
"TOAST_PEER_JOINED": "{name} ist dem Raum beigetreten",
"TOAST_PEER_LEFT": "{name} hat den Raum verlassen",
"TOAST_PEER_ACTION": "{name} hat {action}",
"STATUS_CONNECTED": "Verbunden",
"STATUS_RECONNECTING": "Verbinde erneut...",
"STATUS_CONNECTING": "Verbinde...",
"STATUS_FAILED": "Fehlgeschlagen",
"STATUS_DISCONNECTED": "Getrennt",
"BTN_STATE_JOINING": "🚀 Trete bei...",
"BTN_STATE_RECONNECTING": "🔄 Verbinde erneut...",
"BTN_STATE_PLAYING": "▶ Spiele ab...",
"BTN_STATE_PAUSING": "⏸ Pausiere...",
"BTN_STATE_SYNCING_GROUP": "Synce zur Gruppe ({time})...",
"BTN_STATE_SYNCING": "Synchronisiere...",
"BTN_STATE_SYNCED": "✅ Synchronisiert!",
"NOTIF_PLAY": "die Wiedergabe gestartet",
"NOTIF_PAUSE": "die Wiedergabe pausiert",
"NOTIF_SEEK": "im Video gespult",
"NOTIF_FORCE_PREPARE": "eine erzwungene Synchronisation gestartet",
"NOTIF_FORCE_EXECUTE": "alle Teilnehmer synchronisiert",
"DEBUG_NO_TAB": "Kein Ziel-Tab ausgewählt.",
"DEBUG_COMM_FAIL": "Kommunikation mit dem Tab-Video fehlgeschlagen.",
"EMPTY_PEERS_TITLE": "Noch keine Teilnehmer",
"EMPTY_PEERS_HINT": "Teile deinen Einladungslink, um loszulegen",
"EMPTY_HISTORY_TITLE": "Noch keine Aktivität",
"EMPTY_HISTORY_HINT": "Abspielen, Pausieren oder Spulen, um Verlauf zu sehen",
"EMPTY_LOGS_TITLE": "Keine Logs",
"EMPTY_LOGS_HINT": "Verbindungsereignisse werden hier angezeigt",
"EMPTY_ROOMS_TITLE": "Keine aktiven Räume",
"EMPTY_ROOMS_HINT": "Raum erstellen oder aktualisieren, um öffentliche zu finden"
}
+177
View File
@@ -0,0 +1,177 @@
{
"LANG_CODE": "en",
"HTML_CLASS": "lang-en",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Room",
"TAB_ROOM_TOOLTIP": "Room settings and connection",
"TAB_SYNC": "Sync",
"TAB_SYNC_TOOLTIP": "Video sync controls and remote actions",
"TAB_SETTINGS": "Settings",
"TAB_SETTINGS_TOOLTIP": "Extension preferences",
"TAB_STATUS": "Status",
"TAB_STATUS_TOOLTIP": "Advanced Diagnostics & Logs",
"BTN_CREATE_ROOM": "+ Create New Room",
"MANUAL_CONNECT_HEADER": "Manual Connect / Advanced",
"LABEL_SERVER": "Server",
"BTN_SERVER_OFFICIAL": "Official",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Use the official reliable server",
"BTN_SERVER_CUSTOM": "Custom",
"BTN_SERVER_CUSTOM_TOOLTIP": "Connect to your own self-hosted server",
"PLACEHOLDER_SERVER_URL": "wss://your-server:3000",
"LABEL_ROOM_ID": "Room ID",
"LABEL_ROOM_ID_TOOLTIP": "The unique identifier for your sync room",
"PLACEHOLDER_ROOM_ID": "Enter Room ID",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "The unique ID of the room you want to join",
"LABEL_PASSWORD": "Password (Optional)",
"LABEL_PASSWORD_TOOLTIP": "Optional password to restrict room access",
"PLACEHOLDER_PASSWORD": "Room Password (optional)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Password for the room (leave empty if none)",
"BTN_JOIN_ROOM": "Join Room",
"BTN_JOIN_ROOM_TOOLTIP": "Connect to the room",
"LABEL_PUBLIC_ROOMS": "Public Rooms",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "List of publicly available rooms on this server",
"BTN_REFRESH": "REFRESH",
"BTN_REFRESH_TOOLTIP": "Refresh the list of public rooms",
"PUBLIC_ROOMS_REFRESHING": "Refreshing...",
"LABEL_ACTIVE_ROOM": "Active Room",
"LABEL_ACTIVE_ROOM_TOOLTIP": "The room you are currently connected to",
"ACTIVE_ROOM_NONE": "NONE",
"ACTIVE_SERVER_OFFICIAL": "Official Server",
"LABEL_INVITE_LINK": "Invite Link",
"LABEL_INVITE_LINK_TOOLTIP": "Share this link with friends so they can join",
"LABEL_PEERS_IN_ROOM": "Peers in Room",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Other users currently connected to this room",
"NO_PEERS_CONNECTED": "No peers connected",
"BTN_LEAVE_ROOM": "Leave Room",
"LABEL_SELECT_VIDEO": "Select Video",
"LABEL_SELECT_VIDEO_TOOLTIP": "Choose the browser tab containing the video to sync",
"OPTION_SELECT_TAB": "-- Select a Tab --",
"LABEL_REMOTE_CONTROL": "Remote Control",
"BTN_COPY_INVITE": "📋 Invite Link",
"BTN_COPY_INVITE_TOOLTIP": "Copy Invite Link",
"BTN_PLAY": "▶ Play",
"BTN_PLAY_TOOLTIP": "Send a Play command to everyone",
"BTN_PAUSE": "⏸ Pause",
"BTN_PAUSE_TOOLTIP": "Send a Pause command to everyone",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Force all users to sync up",
"OPTION_JUMP_TO_OTHERS": "Jump to Others",
"OPTION_JUMP_TO_ME": "Jump to Me",
"OPTION_JUMP_MODE_TOOLTIP": "Choose sync target",
"LABEL_LAST_ACTIVITY": "Last Activity Status",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Shows the most recent play, pause, or seek command",
"NO_RECENT_COMMANDS": "No recent commands",
"LOBBY_HEADER": "EPISODE LOBBY",
"LOBBY_WAITING_FOR": "🎬 Waiting for: \"{title}\"",
"LOBBY_WAITING_PEERS": "Waiting for peers...",
"BTN_SKIP_PLAY": "Skip & Play anyway",
"BTN_SKIP_PLAY_TOOLTIP": "Cancel lobby and play anyway",
"LOBBY_CONNECT_FIRST": "Connect to a room first",
"LOBBY_CONNECT_FIRST_DESC": "You need to join a room via an invite link or create a new one to sync videos.",
"BTN_CREATE_ROOM_ALT": "Create New Room",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Create a new random room and join it",
"LABEL_USERNAME": "Your Username",
"LABEL_USERNAME_TOOLTIP": "Username helps others identify you.",
"PLACEHOLDER_USERNAME": "Anonymous Koala",
"LABEL_HIDE_CLUTTER": "Hide Clutter Tabs",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filters out non-video tabs and unrelated domains to keep the list clean",
"LABEL_AUTO_SYNC_NEXT": "Auto-Sync Next Episode",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pauses automatically and waits for all peers when an episode changes, then sync-starts together.",
"LABEL_AUTO_COPY_INVITE": "Auto-Copy Invite Link",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Automatically copies the invite link to your clipboard when creating a new room.",
"LABEL_NOTIFICATIONS": "Browser Notifications",
"LABEL_NOTIFICATIONS_TOOLTIP": "Shows native system notifications when someone joins/leaves or plays/pauses.",
"LABEL_LANGUAGE": "App Language",
"LABEL_LANGUAGE_TOOLTIP": "Choose your preferred extension language",
"LABEL_TROUBLESHOOTING": "Troubleshooting",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Tools for fixing connection issues",
"BTN_REGEN_ID": "Regenerate Peer ID",
"BTN_REGEN_ID_TOOLTIP": "Regenerate your internal ID and reconnect",
"REGEN_ID_DESC": "Use this if you see \"Duplicate Identity\" errors.",
"LABEL_CONN_STATUS": "Connection Status",
"LABEL_CONN_STATUS_TOOLTIP": "Current WebSocket connection state",
"CONN_STATUS_DISCONNECTED": "Disconnected",
"BTN_RETRY": "RETRY",
"BTN_RETRY_TOOLTIP": "Attempt to reconnect to the server",
"BTN_COPY_LOGS": "Copy Logs",
"BTN_COPY_LOGS_TOOLTIP": "Copy logs to clipboard for sharing",
"LABEL_VIDEO_DEBUG": "Video Debug Info",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Technical details about the currently selected video element",
"VIDEO_DEBUG_EMPTY": "No tab selected or video detected.",
"LABEL_HISTORY": "Full Action History",
"LABEL_HISTORY_TOOLTIP": "Chronological log of all sync commands in the room",
"HISTORY_EMPTY": "No activity yet",
"LABEL_LOGS": "Logs (Last 50)",
"LABEL_LOGS_TOOLTIP": "Technical connection logs for debugging",
"BTN_CLEAR": "CLEAR",
"BTN_CLEAR_TOOLTIP": "Clear log output",
"LABEL_GITHUB": "GitHub Repository",
"BTN_ONBOARDING_SKIP": "Skip",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Skip the tutorial",
"BTN_ONBOARDING_NEXT": "Next",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Go to next step",
"ONBOARDING_1_TITLE": "Welcome to KoalaSync!",
"ONBOARDING_1_TEXT": "Watch videos together in perfect sync — no matter where you are. Let's take a quick tour!",
"ONBOARDING_2_TITLE": "1. Create a Room",
"ONBOARDING_2_TEXT": "Start here. Create a room and share the invite link with your friends.",
"ONBOARDING_3_TITLE": "2. Select Video",
"ONBOARDING_3_TEXT": "Navigate here to select the video you want to sync. Play, pause, and seek — everyone stays in sync.",
"ONBOARDING_4_TITLE": "3. Personalize",
"ONBOARDING_4_TEXT": "Pick a fun username so your friends know who you are.",
"ONBOARDING_5_TITLE": "You're all set!",
"ONBOARDING_5_TEXT": "Time to grab some popcorn. Enjoy watching together!",
"ERR_CONN_TIMEOUT": "Connection timed out. Please try again.",
"ERR_INVALID_SERVER_URL": "Invalid Server URL format.",
"ERR_IDENTITY_NOT_LOADED": "Identity not yet loaded. Wait a moment and try again.",
"ERR_NO_PEERS_TIME": "No other peers with a known time. Switch to 'Jump to Me'.",
"ERR_NO_VIDEO_TAB": "Could not connect to video tab.",
"ERR_SELECT_VIDEO": "Please select a video first!",
"TOAST_INVITE_COPIED": "Invite link copied!",
"TOAST_COPY_FAILED": "Failed to copy to clipboard",
"TOAST_LOBBY_SKIPPED": "Episode Lobby skipped.",
"TOAST_LOBBY_SKIP_FAILED": "Failed to skip lobby.",
"TOAST_LOGS_COPIED": "Copied!",
"TOAST_PEER_JOINED": "{name} joined the room",
"TOAST_PEER_LEFT": "{name} left the room",
"TOAST_PEER_ACTION": "{name} {action}",
"STATUS_CONNECTED": "Connected",
"STATUS_RECONNECTING": "Reconnecting...",
"STATUS_CONNECTING": "Connecting...",
"STATUS_FAILED": "Failed",
"STATUS_DISCONNECTED": "Disconnected",
"BTN_STATE_JOINING": "🚀 Joining...",
"BTN_STATE_RECONNECTING": "🔄 Reconnecting...",
"BTN_STATE_PLAYING": "▶ Playing...",
"BTN_STATE_PAUSING": "⏸ Pausing...",
"BTN_STATE_SYNCING_GROUP": "Syncing to group ({time})...",
"BTN_STATE_SYNCING": "Syncing...",
"BTN_STATE_SYNCED": "✅ Synced!",
"NOTIF_PLAY": "started playback",
"NOTIF_PAUSE": "paused playback",
"NOTIF_SEEK": "seeked the video",
"NOTIF_FORCE_PREPARE": "started force sync",
"NOTIF_FORCE_EXECUTE": "synchronized everyone",
"DEBUG_NO_TAB": "No target tab selected.",
"DEBUG_COMM_FAIL": "Could not communicate with tab video.",
"EMPTY_PEERS_TITLE": "No peers yet",
"EMPTY_PEERS_HINT": "Share your invite link to get started",
"EMPTY_HISTORY_TITLE": "No activity yet",
"EMPTY_HISTORY_HINT": "Play, pause, or seek to see history",
"EMPTY_LOGS_TITLE": "No logs",
"EMPTY_LOGS_HINT": "Connection events will appear here",
"EMPTY_ROOMS_TITLE": "No active rooms",
"EMPTY_ROOMS_HINT": "Create a room or refresh to find public ones"
}
+177
View File
@@ -0,0 +1,177 @@
{
"LANG_CODE": "es",
"HTML_CLASS": "lang-es",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Sala",
"TAB_ROOM_TOOLTIP": "Configuración de sala y conexión",
"TAB_SYNC": "Sincro",
"TAB_SYNC_TOOLTIP": "Controles de sincronización de video y acciones",
"TAB_SETTINGS": "Opciones",
"TAB_SETTINGS_TOOLTIP": "Preferencias de la extensión",
"TAB_STATUS": "Estado",
"TAB_STATUS_TOOLTIP": "Diagnósticos avanzados y registros",
"BTN_CREATE_ROOM": "+ Crear nueva sala",
"MANUAL_CONNECT_HEADER": "Conexión manual / Avanzado",
"LABEL_SERVER": "Servidor",
"BTN_SERVER_OFFICIAL": "Oficial",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Usar el servidor oficial confiable",
"BTN_SERVER_CUSTOM": "Perso",
"BTN_SERVER_CUSTOM_TOOLTIP": "Conectarse a su propio servidor auto-alojado",
"PLACEHOLDER_SERVER_URL": "wss://tu-servidor:3000",
"LABEL_ROOM_ID": "ID de sala",
"LABEL_ROOM_ID_TOOLTIP": "El identificador único para tu sala de sincronización",
"PLACEHOLDER_ROOM_ID": "Ingresa el ID de la sala",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "El ID único de la sala a la que deseas unirte",
"LABEL_PASSWORD": "Contraseña (Opcional)",
"LABEL_PASSWORD_TOOLTIP": "Contraseña opcional para restringir el acceso a la sala",
"PLACEHOLDER_PASSWORD": "Contraseña de la sala (opcional)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Contraseña para la sala (dejar vacío si no hay)",
"BTN_JOIN_ROOM": "Unirse a la sala",
"BTN_JOIN_ROOM_TOOLTIP": "Conectarse a la sala",
"LABEL_PUBLIC_ROOMS": "Salas públicas",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Lista de salas públicas disponibles en este servidor",
"BTN_REFRESH": "ACTUALIZAR",
"BTN_REFRESH_TOOLTIP": "Actualizar la lista de salas públicas",
"PUBLIC_ROOMS_REFRESHING": "Actualizando...",
"LABEL_ACTIVE_ROOM": "Sala activa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "La sala a la que estás conectado actualmente",
"ACTIVE_ROOM_NONE": "NINGUNA",
"ACTIVE_SERVER_OFFICIAL": "Servidor oficial",
"LABEL_INVITE_LINK": "Enlace de invitación",
"LABEL_INVITE_LINK_TOOLTIP": "Comparte este enlace con amigos para que se unan",
"LABEL_PEERS_IN_ROOM": "Participantes en la sala",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Otros usuarios conectados actualmente a esta sala",
"NO_PEERS_CONNECTED": "Sin participantes conectados",
"BTN_LEAVE_ROOM": "Salir de la sala",
"LABEL_SELECT_VIDEO": "Seleccionar video",
"LABEL_SELECT_VIDEO_TOOLTIP": "Elige la pestaña del navegador que contiene el video a sincronizar",
"OPTION_SELECT_TAB": "-- Selecciona una pestaña --",
"LABEL_REMOTE_CONTROL": "Control remoto",
"BTN_COPY_INVITE": "📋 Enlace de invitación",
"BTN_COPY_INVITE_TOOLTIP": "Copiar enlace de invitación",
"BTN_PLAY": "▶ Reproducir",
"BTN_PLAY_TOOLTIP": "Enviar un comando de reproducción a todos",
"BTN_PAUSE": "⏸ Pausar",
"BTN_PAUSE_TOOLTIP": "Enviar un comando de pausa a todos",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Forzar la sincronización de todos los usuarios",
"OPTION_JUMP_TO_OTHERS": "Unirse a otros",
"OPTION_JUMP_TO_ME": "Traerlos a mí",
"OPTION_JUMP_MODE_TOOLTIP": "Elegir el objetivo de sincronización",
"LABEL_LAST_ACTIVITY": "Última actividad",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Muestra el comando más reciente de reproducción, pausa o búsqueda",
"NO_RECENT_COMMANDS": "Sin comandos recientes",
"LOBBY_HEADER": "SALA DE ESPERA DE EPISODIO",
"LOBBY_WAITING_FOR": "🎬 Esperando a: \"{title}\"",
"LOBBY_WAITING_PEERS": "Esperando participantes...",
"BTN_SKIP_PLAY": "Omitir y reproducir",
"BTN_SKIP_PLAY_TOOLTIP": "Cancelar la sala de espera y reproducir de todos modos",
"LOBBY_CONNECT_FIRST": "Únete a una sala primero",
"LOBBY_CONNECT_FIRST_DESC": "Necesitas unirte a una sala a través de un enlace de invitación o crear una nueva para sincronizar videos.",
"BTN_CREATE_ROOM_ALT": "Crear nueva sala",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Crear una nueva sala aleatoria y unirse",
"LABEL_USERNAME": "Tu nombre de usuario",
"LABEL_USERNAME_TOOLTIP": "El nombre de usuario ayuda a otros a identificarte.",
"PLACEHOLDER_USERNAME": "Koala anónimo",
"LABEL_HIDE_CLUTTER": "Lista de pestañas limpia",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra pestañas que no son de video y dominios no relacionados para mantener limpia la lista",
"LABEL_AUTO_SYNC_NEXT": "Sincro auto del siguiente episodio",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automáticamente y espera a todos al cambiar de episodio, luego inicia de forma sincrónica.",
"LABEL_AUTO_COPY_INVITE": "Auto-copiar enlace",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia automáticamente el enlace de invitación a tu portapapeles al crear una nueva sala.",
"LABEL_NOTIFICATIONS": "Notificaciones del navegador",
"LABEL_NOTIFICATIONS_TOOLTIP": "Muestra notificaciones del sistema cuando alguien se une/sale o reproduce/pausa.",
"LABEL_LANGUAGE": "Idioma de la aplicación",
"LABEL_LANGUAGE_TOOLTIP": "Elige tu idioma preferido para la extensión",
"LABEL_TROUBLESHOOTING": "Resolución de problemas",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Herramientas para solucionar problemas de conexión",
"BTN_REGEN_ID": "Regenerar ID de participante",
"BTN_REGEN_ID_TOOLTIP": "Regenerar tu ID interno y volver a conectarte",
"REGEN_ID_DESC": "Usa esto si ves errores de 'Identidad duplicada'.",
"LABEL_CONN_STATUS": "Estado de la conexión",
"LABEL_CONN_STATUS_TOOLTIP": "Estado actual de la conexión WebSocket",
"CONN_STATUS_DISCONNECTED": "Desconectado",
"BTN_RETRY": "REINTENTAR",
"BTN_RETRY_TOOLTIP": "Intentar volver a conectarse al servidor",
"BTN_COPY_LOGS": "Copiar registros",
"BTN_COPY_LOGS_TOOLTIP": "Copiar registros al portapapeles para compartir",
"LABEL_VIDEO_DEBUG": "Información de depuración de video",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Detalles técnicos sobre el elemento de video seleccionado actualmente",
"VIDEO_DEBUG_EMPTY": "No hay pestaña seleccionada o no se detectó ningún video.",
"LABEL_HISTORY": "Historial completo",
"LABEL_HISTORY_TOOLTIP": "Registro cronológico de todos los comandos de sincronización en la sala",
"HISTORY_EMPTY": "Sin actividad aún",
"LABEL_LOGS": "Logs (Últimos 50)",
"LABEL_LOGS_TOOLTIP": "Registros técnicos de conexión para depuración",
"BTN_CLEAR": "LIMPIAR",
"BTN_CLEAR_TOOLTIP": "Limpiar salida de registros",
"LABEL_GITHUB": "Repositorio de GitHub",
"BTN_ONBOARDING_SKIP": "Omitir",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Omitir el tutorial",
"BTN_ONBOARDING_NEXT": "Siguiente",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Ir al siguiente paso",
"ONBOARDING_1_TITLE": "¡Bienvenido a KoalaSync!",
"ONBOARDING_1_TEXT": "Mira videos juntos en perfecta sincronización, sin importar dónde estén. ¡Hagamos un recorrido rápido!",
"ONBOARDING_2_TITLE": "1. Crear una sala",
"ONBOARDING_2_TEXT": "Comienza aquí. Crea una sala y comparte el enlace de invitación con tus amigos.",
"ONBOARDING_3_TITLE": "2. Seleccionar video",
"ONBOARDING_3_TEXT": "Navega aquí para seleccionar el video que deseas sincronizar. Reproduce, pausa y busca: todos permanecen sincronizados.",
"ONBOARDING_4_TITLE": "3. Personalizar",
"ONBOARDING_4_TEXT": "Elige un nombre de usuario divertido para que tus amigos sepan quién eres.",
"ONBOARDING_5_TITLE": "¡Todo listo!",
"ONBOARDING_5_TEXT": "Hora de preparar las palomitas. ¡Disfruta viendo con tus amigos!",
"ERR_CONN_TIMEOUT": "Tiempo de conexión agotado. Inténtalo de nuevo.",
"ERR_INVALID_SERVER_URL": "Formato de URL de servidor no válido.",
"ERR_IDENTITY_NOT_LOADED": "Identidad no cargada aún. Espera un momento e inténtalo de nuevo.",
"ERR_NO_PEERS_TIME": "No hay otros participantes con posición conocida. Cambia a 'Traerlos a mí'.",
"ERR_NO_VIDEO_TAB": "No se pudo conectar a la pestaña de video.",
"ERR_SELECT_VIDEO": "¡Selecciona un video primero!",
"TOAST_INVITE_COPIED": "¡Enlace de invitación copiado!",
"TOAST_COPY_FAILED": "Error al copiar al portapapeles",
"TOAST_LOBBY_SKIPPED": "Sala de espera de episodio omitida.",
"TOAST_LOBBY_SKIP_FAILED": "Error al omitir la sala de espera.",
"TOAST_LOGS_COPIED": "¡Copiado!",
"TOAST_PEER_JOINED": "{name} se unió a la sala",
"TOAST_PEER_LEFT": "{name} salió de la sala",
"TOAST_PEER_ACTION": "{name} ha {action}",
"STATUS_CONNECTED": "Conectado",
"STATUS_RECONNECTING": "Reconectando...",
"STATUS_CONNECTING": "Conectando...",
"STATUS_FAILED": "Error",
"STATUS_DISCONNECTED": "Desconectado",
"BTN_STATE_JOINING": "🚀 Uniéndose...",
"BTN_STATE_RECONNECTING": "🔄 Reconectando...",
"BTN_STATE_PLAYING": "▶ Reproduciendo...",
"BTN_STATE_PAUSING": "⏸ Pausando...",
"BTN_STATE_SYNCING_GROUP": "Sincronizando al grupo ({time})...",
"BTN_STATE_SYNCING": "Sincronizando...",
"BTN_STATE_SYNCED": "✅ ¡Sincronizado!",
"NOTIF_PLAY": "inició la reproducción",
"NOTIF_PAUSE": "pausó la reproducción",
"NOTIF_SEEK": "cambió la posición del video",
"NOTIF_FORCE_PREPARE": "inició una sincronización forzada",
"NOTIF_FORCE_EXECUTE": "sincronizó a todos",
"DEBUG_NO_TAB": "No hay pestaña objetivo seleccionada.",
"DEBUG_COMM_FAIL": "No se pudo comunicar con el video de la pestaña.",
"EMPTY_PEERS_TITLE": "Sin participantes aún",
"EMPTY_PEERS_HINT": "Comparte tu enlace de invitación para comenzar",
"EMPTY_HISTORY_TITLE": "Sin actividad aún",
"EMPTY_HISTORY_HINT": "Reproduce, pausa o busca para ver el historial",
"EMPTY_LOGS_TITLE": "Sin registros",
"EMPTY_LOGS_HINT": "Los eventos de conexión aparecerán aquí",
"EMPTY_ROOMS_TITLE": "Sin salas activas",
"EMPTY_ROOMS_HINT": "Crea una sala o actualiza para buscar salas públicas"
}
+177
View File
@@ -0,0 +1,177 @@
{
"LANG_CODE": "fr",
"HTML_CLASS": "lang-fr",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Salon",
"TAB_ROOM_TOOLTIP": "Paramètres de salon et connexion",
"TAB_SYNC": "Synchro",
"TAB_SYNC_TOOLTIP": "Contrôles de synchronisation vidéo et actions",
"TAB_SETTINGS": "Options",
"TAB_SETTINGS_TOOLTIP": "Préférences de l'extension",
"TAB_STATUS": "Statut",
"TAB_STATUS_TOOLTIP": "Diagnostics avancés & Journaux",
"BTN_CREATE_ROOM": "+ Créer un nouveau salon",
"MANUAL_CONNECT_HEADER": "Connexion manuelle / Avancé",
"LABEL_SERVER": "Serveur",
"BTN_SERVER_OFFICIAL": "Officiel",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Utiliser le serveur officiel fiable",
"BTN_SERVER_CUSTOM": "Perso",
"BTN_SERVER_CUSTOM_TOOLTIP": "Se connecter à votre propre serveur auto-hébergé",
"PLACEHOLDER_SERVER_URL": "wss://votre-serveur:3000",
"LABEL_ROOM_ID": "ID du salon",
"LABEL_ROOM_ID_TOOLTIP": "L'identifiant unique de votre salon de synchronisation",
"PLACEHOLDER_ROOM_ID": "Entrer l'ID du salon",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "L'ID unique du salon que vous souhaitez rejoindre",
"LABEL_PASSWORD": "Mot de passe (Optionnel)",
"LABEL_PASSWORD_TOOLTIP": "Mot de passe optionnel pour restreindre l'accès au salon",
"PLACEHOLDER_PASSWORD": "Mot de passe du salon (optionnel)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Mot de passe du salon (laisser vide si aucun)",
"BTN_JOIN_ROOM": "Rejoindre le salon",
"BTN_JOIN_ROOM_TOOLTIP": "Se connecter au salon",
"LABEL_PUBLIC_ROOMS": "Salons publics",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Liste des salons publics disponibles sur ce serveur",
"BTN_REFRESH": "ACTUALISER",
"BTN_REFRESH_TOOLTIP": "Actualiser la liste des salons publics",
"PUBLIC_ROOMS_REFRESHING": "Actualisation...",
"LABEL_ACTIVE_ROOM": "Salon actif",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Le salon auquel vous êtes actuellement connecté",
"ACTIVE_ROOM_NONE": "AUCUN",
"ACTIVE_SERVER_OFFICIAL": "Serveur officiel",
"LABEL_INVITE_LINK": "Lien d'invitation",
"LABEL_INVITE_LINK_TOOLTIP": "Partagez ce lien avec vos amis pour qu'ils vous rejoignent",
"LABEL_PEERS_IN_ROOM": "Membres dans le salon",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Autres utilisateurs connectés à ce salon",
"NO_PEERS_CONNECTED": "Aucun membre connecté",
"BTN_LEAVE_ROOM": "Quitter le salon",
"LABEL_SELECT_VIDEO": "Choisir une vidéo",
"LABEL_SELECT_VIDEO_TOOLTIP": "Choisissez l'onglet du navigateur contenant la vidéo à synchroniser",
"OPTION_SELECT_TAB": "-- Choisir un onglet --",
"LABEL_REMOTE_CONTROL": "Contrôle à distance",
"BTN_COPY_INVITE": "📋 Lien d'invitation",
"BTN_COPY_INVITE_TOOLTIP": "Copier le lien d'invitation",
"BTN_PLAY": "▶ Lecture",
"BTN_PLAY_TOOLTIP": "Envoyer une commande Lecture à tout le monde",
"BTN_PAUSE": "⏸ Pause",
"BTN_PAUSE_TOOLTIP": "Envoyer une commande Pause à tout le monde",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Forcer tous les utilisateurs à se synchroniser",
"OPTION_JUMP_TO_OTHERS": "Rejoindre les autres",
"OPTION_JUMP_TO_ME": "Les amener à moi",
"OPTION_JUMP_MODE_TOOLTIP": "Choisir la cible de synchronisation",
"LABEL_LAST_ACTIVITY": "Dernière activité",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Affiche la dernière commande de lecture, pause ou recherche",
"NO_RECENT_COMMANDS": "Aucune commande récente",
"LOBBY_HEADER": "LOBBY D'ÉPISODE",
"LOBBY_WAITING_FOR": "🎬 En attente de : \"{title}\"",
"LOBBY_WAITING_PEERS": "En attente des membres...",
"BTN_SKIP_PLAY": "Ignorer & Lancer",
"BTN_SKIP_PLAY_TOOLTIP": "Annuler le lobby et lancer la lecture quand même",
"LOBBY_CONNECT_FIRST": "Rejoignez d'abord un salon",
"LOBBY_CONNECT_FIRST_DESC": "Vous devez rejoindre un salon via un lien d'invitation ou en créer un nouveau pour synchroniser des vidéos.",
"BTN_CREATE_ROOM_ALT": "Créer un nouveau salon",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Créer un salon aléatoire et le rejoindre",
"LABEL_USERNAME": "Votre pseudo",
"LABEL_USERNAME_TOOLTIP": "Votre pseudo permet aux autres de vous identifier.",
"PLACEHOLDER_USERNAME": "Koala anonyme",
"LABEL_HIDE_CLUTTER": "Liste d'onglets épurée",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtre les onglets non-vidéo et les domaines non pertinents pour garder la liste propre",
"LABEL_AUTO_SYNC_NEXT": "Synchro auto l'épisode suivant",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Met en pause et attend tous les membres lors d'un changement d'épisode, puis démarre de manière synchrone.",
"LABEL_AUTO_COPY_INVITE": "Copie auto du lien",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copie automatiquement le lien d'invitation dans votre presse-papiers lors de la création d'un salon.",
"LABEL_NOTIFICATIONS": "Notifications de navigateur",
"LABEL_NOTIFICATIONS_TOOLTIP": "Affiche des notifications système lorsqu'un membre arrive/part ou lance/met en pause la lecture.",
"LABEL_LANGUAGE": "Langue de l'application",
"LABEL_LANGUAGE_TOOLTIP": "Choisissez la langue de l'application",
"LABEL_TROUBLESHOOTING": "Dépannage",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Outils pour résoudre les problèmes de connexion",
"BTN_REGEN_ID": "Régénérer l'identifiant",
"BTN_REGEN_ID_TOOLTIP": "Régénérer votre identifiant interne et vous reconnecter",
"REGEN_ID_DESC": "Utilisez cette option si vous rencontrez des erreurs de 'Double identité'.",
"LABEL_CONN_STATUS": "Statut de la connexion",
"LABEL_CONN_STATUS_TOOLTIP": "Statut actuel de la connexion WebSocket",
"CONN_STATUS_DISCONNECTED": "Déconnecté",
"BTN_RETRY": "RÉESSAYER",
"BTN_RETRY_TOOLTIP": "Tenter de se reconnecter au serveur",
"BTN_COPY_LOGS": "Copier les journaux",
"BTN_COPY_LOGS_TOOLTIP": "Copier les journaux dans le presse-papiers",
"LABEL_VIDEO_DEBUG": "Infos de débogage vidéo",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Détails techniques de l'élément vidéo actuellement sélectionné",
"VIDEO_DEBUG_EMPTY": "Aucun onglet sélectionné ou aucune vidéo détectée.",
"LABEL_HISTORY": "Historique complet",
"LABEL_HISTORY_TOOLTIP": "Historique chronologique de toutes les commandes de synchronisation dans le salon",
"HISTORY_EMPTY": "Aucune activité pour le moment",
"LABEL_LOGS": "Journaux (50 derniers)",
"LABEL_LOGS_TOOLTIP": "Journaux techniques pour le débogage de la connexion",
"BTN_CLEAR": "EFFACER",
"BTN_CLEAR_TOOLTIP": "Effacer la sortie des journaux",
"LABEL_GITHUB": "Dépôt GitHub",
"BTN_ONBOARDING_SKIP": "Passer",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Passer le didacticiel",
"BTN_ONBOARDING_NEXT": "Suivant",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Étape suivante",
"ONBOARDING_1_TITLE": "Bienvenue sur KoalaSync !",
"ONBOARDING_1_TEXT": "Regardez des vidéos en parfaite synchronisation — où que vous soyez. Faisons un petit tour !",
"ONBOARDING_2_TITLE": "1. Créer un salon",
"ONBOARDING_2_TEXT": "Commencez ici. Créez un salon et partagez le lien d'invitation avec vos amis.",
"ONBOARDING_3_TITLE": "2. Choisir une vidéo",
"ONBOARDING_3_TEXT": "Naviguez ici pour sélectionner la vidéo à synchroniser. Lecture, pause, recherche — tout le monde reste synchrone.",
"ONBOARDING_4_TITLE": "3. Personnaliser",
"ONBOARDING_4_TEXT": "Choisissez un pseudo sympa pour que vos amis sachent qui vous êtes.",
"ONBOARDING_5_TITLE": "Vous êtes prêt !",
"ONBOARDING_5_TEXT": "Il est temps de préparer le pop-corn. Bon visionnage !",
"ERR_CONN_TIMEOUT": "Délai de connexion dépassé. Veuillez réessayer.",
"ERR_INVALID_SERVER_URL": "Format d'adresse de serveur non valide.",
"ERR_IDENTITY_NOT_LOADED": "Identifiant non encore chargé. Veuillez patienter et réessayer.",
"ERR_NO_PEERS_TIME": "Aucun autre membre avec une position connue. Basculez sur 'Les amener à moi'.",
"ERR_NO_VIDEO_TAB": "Impossible de se connecter à l'onglet vidéo.",
"ERR_SELECT_VIDEO": "Veuillez d'abord sélectionner une vidéo !",
"TOAST_INVITE_COPIED": "Lien d'invitation copié !",
"TOAST_COPY_FAILED": "Échec de la copie dans le presse-papiers",
"TOAST_LOBBY_SKIPPED": "Lobby d'épisode ignoré.",
"TOAST_LOBBY_SKIP_FAILED": "Échec de l'annulation du lobby.",
"TOAST_LOGS_COPIED": "Copié !",
"TOAST_PEER_JOINED": "{name} a rejoint le salon",
"TOAST_PEER_LEFT": "{name} a quitté le salon",
"TOAST_PEER_ACTION": "{name} a {action}",
"STATUS_CONNECTED": "Connecté",
"STATUS_RECONNECTING": "Reconnexion...",
"STATUS_CONNECTING": "Connexion...",
"STATUS_FAILED": "Échec",
"STATUS_DISCONNECTED": "Déconnecté",
"BTN_STATE_JOINING": "🚀 Connexion...",
"BTN_STATE_RECONNECTING": "🔄 Reconnexion...",
"BTN_STATE_PLAYING": "▶ Lecture...",
"BTN_STATE_PAUSING": "⏸ Pause...",
"BTN_STATE_SYNCING_GROUP": "Synchro au groupe ({time})...",
"BTN_STATE_SYNCING": "Synchronisation...",
"BTN_STATE_SYNCED": "✅ Synchronisé !",
"NOTIF_PLAY": "lancé la lecture",
"NOTIF_PAUSE": "mis la lecture en pause",
"NOTIF_SEEK": "déplacé la position dans la vidéo",
"NOTIF_FORCE_PREPARE": "lancé une synchronisation forcée",
"NOTIF_FORCE_EXECUTE": "synchronisé tout le monde",
"DEBUG_NO_TAB": "Aucun onglet cible sélectionné.",
"DEBUG_COMM_FAIL": "Impossible de communiquer avec l'onglet vidéo.",
"EMPTY_PEERS_TITLE": "Aucun membre pour l'instant",
"EMPTY_PEERS_HINT": "Partagez votre lien d'invitation pour commencer",
"EMPTY_HISTORY_TITLE": "Aucune activité pour l'instant",
"EMPTY_HISTORY_HINT": "Lancez, mettez en pause ou déplacez la position pour voir l'historique",
"EMPTY_LOGS_TITLE": "Aucun journal",
"EMPTY_LOGS_HINT": "Les événements de connexion s'afficheront ici",
"EMPTY_ROOMS_TITLE": "Aucun salon actif",
"EMPTY_ROOMS_HINT": "Créez un salon ou actualisez pour trouver des salons publics"
}
+204
View File
@@ -0,0 +1,204 @@
# KoalaSync Browser Extension v2.0 - i18n Technical Implementation Plan
Welcome, future Antigravity AI agent! This document is placed directly in the codebase at `/extension/locales/i18n_plan.md` to serve as a comprehensive architectural handbook for the next steps in adding **full internationalization (i18n) support to the browser extension itself** while maintaining 100% video-sync and background communication safety.
---
## 🔍 Context & Audited Scope
KoalaSync is a lightweight, premium browser extension (Chrome & Firefox Manifest V3) for synchronized video playback. The landing pages are already compiled dynamically in 6 languages:
* English (`en`)
* German (`de`)
* French (`fr`)
* Spanish (`es`)
* Portuguese (Brazil) (`pt-BR`)
* Russian (`ru`)
Our goal is to build an identical, premium translation engine for the extension itself.
---
## 🏛️ Architectural Choice: Custom JSON Dictionary Engine (Approach B)
We evaluated two paths:
1. **Approach A (Native `chrome.i18n` API):** Uses `_locales/` directory. Too rigid—cannot support real-time dynamic switching inside the extension Settings dropdown without closing/re-opening the popup.
2. **Approach B (Custom Unified JSON Engine):** Uses flat JSON files matching our website files (`"KEY": "Value"`). Dynamically merges the target dictionary with baseline English `en.json` at runtime, programmatically providing an **airtight English fallback** and **real-time DOM translations** without popup reload.
We chose **Approach B** for maximum compatibility, premium real-time toggling, and clean fallback safety.
---
## 🔄 Resolve, Load, & State Flow
1. **On launch:** Look for saved language in `chrome.storage.sync.get('locale')`.
2. **Fallback Autodetect:** If no saved language, read `navigator.language` or `chrome.i18n.getUILanguage()`.
* If the detected locale is supported, set as active.
* If not supported, default to English (`en`).
3. **Dictionary Resolution:**
* Asynchronously load the English baseline dictionary (`/extension/locales/en.json`).
* If the target language is different, load target JSON (e.g. `/extension/locales/de.json`) and execute `Object.assign({}, enDict, targetDict)`. This guarantees dynamic translation while cleanly falling back to English for any missing keys.
4. **DOM Replacements:** Scan for `data-i18n`, `data-i18n-title`, and `data-i18n-placeholder` attributes, and translate them on the fly.
5. **Persistence:** Save dynamic selection modifications from the dropdown into `chrome.storage.sync`. Trigger instant DOM re-translation on change.
---
## 📂 Proposed File Structure
```
KoalaPlay/
└── extension/
├── locales/ # [NEW] Contains flat translation maps
│ ├── i18n_plan.md # This roadmap file
│ ├── en.json # Flat English baseline keys
│ ├── de.json # German keys
│ ├── fr.json # French keys
│ ├── es.json # Spanish keys
│ ├── pt-BR.json # Portuguese (Brasil) keys
│ └── ru.json # Russian keys
├── i18n.js # [NEW] ESM translation engine module
├── popup.html # Modified with data-i18n attributes
├── popup.js # Modified to initialize locales and update variables
└── background.js # Modified to push localized notification alerts
```
---
## 🛠️ Draft Code Snippets
### 1. `i18n.js` (Zero-Dependency Engine Module)
```javascript
// extension/i18n.js
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru'];
export const DEFAULT_LANGUAGE = 'en';
let activeDictionary = {};
export async function loadLocale(langCode) {
const resolvedLang = SUPPORTED_LANGUAGES.includes(langCode) ? langCode : DEFAULT_LANGUAGE;
try {
const enResponse = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
const enDict = await enResponse.json();
if (resolvedLang === DEFAULT_LANGUAGE) {
activeDictionary = enDict;
return;
}
const targetResponse = await fetch(chrome.runtime.getURL(`locales/${resolvedLang}.json`));
const targetDict = await targetResponse.json();
activeDictionary = Object.assign({}, enDict, targetDict);
} catch (err) {
console.error('[i18n] Failed to load dictionary. Falling back to English:', err);
const rescue = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
activeDictionary = await rescue.json();
}
}
export function getMessage(key) {
return activeDictionary[key] || key;
}
export function translateDOM() {
// Translate text nodes
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
const translated = getMessage(key);
const img = el.querySelector('img');
if (img) {
el.innerHTML = '';
el.appendChild(img);
el.appendChild(document.createTextNode(' ' + translated));
} else {
el.textContent = translated;
}
});
// Translate tooltips
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
el.setAttribute('title', getMessage(key));
});
// Translate placeholders
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
el.setAttribute('placeholder', getMessage(key));
});
}
```
### 2. Markup Changes (`popup.html`)
* Annotate text elements: `<button class="tab-btn" data-tab="tab-settings" data-i18n="TAB_SETTINGS" data-i18n-title="TAB_SETTINGS_TOOLTIP">Settings</button>`
* Add Language Dropdown in the Settings Panel:
```html
<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" title="Choose your preferred extension language">App Language</label>
<select id="langSelector" style="width: 150px; padding: 6px 10px; font-size: 13px; cursor: pointer;">
<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>
</div>
```
### 3. Dynamic Script Wiring (`popup.js`)
* Import our engine: `import { loadLocale, translateDOM, getMessage } from './i18n.js';`
* Initialize during startup:
```javascript
async function init() {
const data = await chrome.storage.sync.get(['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';
chrome.storage.sync.set({ locale: activeLang });
}
await loadLocale(activeLang);
translateDOM();
// Select matching option in selector
const langSelector = document.getElementById('langSelector');
if (langSelector) langSelector.value = activeLang;
// rest of standard init...
}
```
* Listen to Settings change event:
```javascript
const langSelector = document.getElementById('langSelector');
if (langSelector) {
langSelector.addEventListener('change', async () => {
const selectedLang = langSelector.value;
await chrome.storage.sync.set({ locale: selectedLang });
await loadLocale(selectedLang);
translateDOM();
// Re-render empty elements and tab list using new dynamic strings
refreshLogs();
refreshHistory();
populateTabs();
});
}
```
### 4. Background Notifications (`background.js`)
* Notifications inside `showNotification` should fetch dynamic keys based on `chrome.storage.sync` language settings and build clean alerts dynamically using `chrome.storage.sync.get('locale')`.
* Ensure that WebSocket event transmissions and video player scripts in `content.js` remain **completely untouched and unaware** of the localization engine.
---
## 🔒 Safety Guardrail
* **WebSocket Engine Integrity:** Under no circumstances should `background.js` Socket.IO handshakes, rate limits, or binary protocol headers be adjusted. i18n is strictly an interface rendering skin layer.
* **Player Control Pipeline Integrity:** `content.js` expectedEvent suppression tables, HLS buffer calculations, and Media Session API listeners must remain untouched. Do not bind any i18n logic into the content script's active tracking loop.
---
## 🧪 Verification Tasks
1. **Autodetection Audit:** Force-load the extension in Chrome/Firefox in a non-English language container and ensure it starts in that language or English gracefully.
2. **Integrity Validation Script:** Write a sanity node checker script `/scripts/test-locales.js` to ensure all key structures are uniform across all JSON files.
3. **Real-Time Refresh Verification:** Change dynamic drop-down preferences and verify that every element and alert swaps languages cleanly with zero redraw glitches or page crashes.
+177
View File
@@ -0,0 +1,177 @@
{
"LANG_CODE": "pt-BR",
"HTML_CLASS": "lang-pt-br",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Sala",
"TAB_ROOM_TOOLTIP": "Configurações da sala e conexão",
"TAB_SYNC": "Sincro",
"TAB_SYNC_TOOLTIP": "Controles de sincronia de vídeo e ações",
"TAB_SETTINGS": "Opções",
"TAB_SETTINGS_TOOLTIP": "Preferências da extensão",
"TAB_STATUS": "Status",
"TAB_STATUS_TOOLTIP": "Diagnósticos avançados e logs",
"BTN_CREATE_ROOM": "+ Criar nova sala",
"MANUAL_CONNECT_HEADER": "Conexão manual / Avançado",
"LABEL_SERVER": "Servidor",
"BTN_SERVER_OFFICIAL": "Oficial",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Usar o servidor oficial confiável",
"BTN_SERVER_CUSTOM": "Perso",
"BTN_SERVER_CUSTOM_TOOLTIP": "Conectar ao seu próprio servidor auto-hospedado",
"PLACEHOLDER_SERVER_URL": "wss://seu-servidor:3000",
"LABEL_ROOM_ID": "ID da sala",
"LABEL_ROOM_ID_TOOLTIP": "O identificador exclusivo para sua sala de sincronização",
"PLACEHOLDER_ROOM_ID": "Digite o ID da sala",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "O ID exclusivo da sala na qual você deseja entrar",
"LABEL_PASSWORD": "Senha (Opcional)",
"LABEL_PASSWORD_TOOLTIP": "Senha opcional para restringir o acesso à sala",
"PLACEHOLDER_PASSWORD": "Senha da sala (opcional)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Senha para a sala (deixe em branco se não houver)",
"BTN_JOIN_ROOM": "Entrar na sala",
"BTN_JOIN_ROOM_TOOLTIP": "Conectar à 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": "Atualizando...",
"LABEL_ACTIVE_ROOM": "Sala ativa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "A sala à qual você está conectado no momento",
"ACTIVE_ROOM_NONE": "NENHUMA",
"ACTIVE_SERVER_OFFICIAL": "Servidor oficial",
"LABEL_INVITE_LINK": "Link de convite",
"LABEL_INVITE_LINK_TOOLTIP": "Compartilhe este link com amigos para que eles entrem",
"LABEL_PEERS_IN_ROOM": "Participantes na sala",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Outros usuários conectados atualmente a esta sala",
"NO_PEERS_CONNECTED": "Sem participantes conectados",
"BTN_LEAVE_ROOM": "Sair da sala",
"LABEL_SELECT_VIDEO": "Selecionar vídeo",
"LABEL_SELECT_VIDEO_TOOLTIP": "Escolha a aba do navegador contendo o vídeo a ser sincronizado",
"OPTION_SELECT_TAB": "-- Selecione uma aba --",
"LABEL_REMOTE_CONTROL": "Controle remoto",
"BTN_COPY_INVITE": "📋 Link de convite",
"BTN_COPY_INVITE_TOOLTIP": "Copiar link de convite",
"BTN_PLAY": "▶ Reproduzir",
"BTN_PLAY_TOOLTIP": "Enviar um comando de reproduzir para todos",
"BTN_PAUSE": "⏸ Pausar",
"BTN_PAUSE_TOOLTIP": "Enviar um comando de pausar para todos",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Forçar todos os usuários a sincronizar",
"OPTION_JUMP_TO_OTHERS": "Ir para os outros",
"OPTION_JUMP_TO_ME": "Trazer para mim",
"OPTION_JUMP_MODE_TOOLTIP": "Escolher o alvo de sincronização",
"LABEL_LAST_ACTIVITY": "Última atividade",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Mostra o comando mais recente de reproduzir, pausar ou buscar",
"NO_RECENT_COMMANDS": "Sem comandos recentes",
"LOBBY_HEADER": "LOBBY DE EPISÓDIO",
"LOBBY_WAITING_FOR": "🎬 Aguardando: \"{title}\"",
"LOBBY_WAITING_PEERS": "Aguardando participantes...",
"BTN_SKIP_PLAY": "Pular e reproduzir",
"BTN_SKIP_PLAY_TOOLTIP": "Cancelar o lobby e reproduzir assim mesmo",
"LOBBY_CONNECT_FIRST": "Entre em uma sala primeiro",
"LOBBY_CONNECT_FIRST_DESC": "Você precisa entrar em uma 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",
"LABEL_USERNAME": "Seu nome de usuário",
"LABEL_USERNAME_TOOLTIP": "O nome de usuário ajuda os outros a identificá-lo.",
"PLACEHOLDER_USERNAME": "Coala anônimo",
"LABEL_HIDE_CLUTTER": "Lista de abas limpa",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra abas que não são de vídeo e domínios não relacionados para manter a lista organizada",
"LABEL_AUTO_SYNC_NEXT": "Sincro auto do próximo episódio",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automaticamente e aguarda todos ao mudar de episódio, depois inicia em sincronia.",
"LABEL_AUTO_COPY_INVITE": "Auto-copiar link",
"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 do aplicativo",
"LABEL_LANGUAGE_TOOLTIP": "Escolha o idioma de sua preferência para a extensão",
"LABEL_TROUBLESHOOTING": "Resolução de problemas",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Ferramentas para solucionar problemas de conexão",
"BTN_REGEN_ID": "Regenerar ID de participante",
"BTN_REGEN_ID_TOOLTIP": "Regenerar seu ID interno e conectar novamente",
"REGEN_ID_DESC": "Use esta opção se notar erros de 'Identidade duplicada'.",
"LABEL_CONN_STATUS": "Status da conexão",
"LABEL_CONN_STATUS_TOOLTIP": "Status atual da conexão WebSocket",
"CONN_STATUS_DISCONNECTED": "Desconectado",
"BTN_RETRY": "REENTRAR",
"BTN_RETRY_TOOLTIP": "Tentar reconectar ao servidor",
"BTN_COPY_LOGS": "Copiar logs",
"BTN_COPY_LOGS_TOOLTIP": "Copiar logs para a área de transferência para compartilhar",
"LABEL_VIDEO_DEBUG": "Informações de depuração de vídeo",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Detalhes técnicos sobre o elemento de vídeo selecionado atualmente",
"VIDEO_DEBUG_EMPTY": "Nenhuma aba selecionada ou vídeo detectado.",
"LABEL_HISTORY": "Histórico completo",
"LABEL_HISTORY_TOOLTIP": "Registro cronológico de todos os comandos de sincronização na sala",
"HISTORY_EMPTY": "Sem atividade ainda",
"LABEL_LOGS": "Logs (Últimos 50)",
"LABEL_LOGS_TOOLTIP": "Logs técnicos de conexão para depuração",
"BTN_CLEAR": "LIMPAR",
"BTN_CLEAR_TOOLTIP": "Limpar a saída dos logs",
"LABEL_GITHUB": "Repositório GitHub",
"BTN_ONBOARDING_SKIP": "Pular",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Pular o tutorial",
"BTN_ONBOARDING_NEXT": "Próximo",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Ir para o próximo passo",
"ONBOARDING_1_TITLE": "Bem-vindo ao KoalaSync!",
"ONBOARDING_1_TEXT": "Assista a vídeos juntos em perfeita sincronia, não importa onde estejam. Vamos fazer um tour rápido!",
"ONBOARDING_2_TITLE": "1. Criar uma sala",
"ONBOARDING_2_TEXT": "Comece aqui. Crie uma sala e compartilhe o link de convite com seus amigos.",
"ONBOARDING_3_TITLE": "2. Selecionar vídeo",
"ONBOARDING_3_TEXT": "Navegue aqui para selecionar o vídeo a ser sincronizado. Reproduza, pause e busque: todos permanecem sincronizados.",
"ONBOARDING_4_TITLE": "3. Personalizar",
"ONBOARDING_4_TEXT": "Escolha um nome de usuário divertido para que seus amigos saibam quem você é.",
"ONBOARDING_5_TITLE": "Tudo pronto!",
"ONBOARDING_5_TEXT": "Hora de pegar a pipoca. Divirta-se assistindo com seus amigos!",
"ERR_CONN_TIMEOUT": "Tempo limite de conexão esgotado. Tente novamente.",
"ERR_INVALID_SERVER_URL": "Formato de URL do servidor inválido.",
"ERR_IDENTITY_NOT_LOADED": "Identidade não carregada ainda. Aguarde um momento e tente novamente.",
"ERR_NO_PEERS_TIME": "Nenhum outro participante com posição conhecida. Mude para 'Traerlos a mí' / 'Trazer para mim'.",
"ERR_NO_VIDEO_TAB": "Não foi possível conectar à aba do vídeo.",
"ERR_SELECT_VIDEO": "Selecione um vídeo primeiro!",
"TOAST_INVITE_COPIED": "Link de convite copiado!",
"TOAST_COPY_FAILED": "Falha ao copiar para a área de transferência",
"TOAST_LOBBY_SKIPPED": "Lobby de 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": "Conectado",
"STATUS_RECONNECTING": "Reconectando...",
"STATUS_CONNECTING": "Conectando...",
"STATUS_FAILED": "Falhou",
"STATUS_DISCONNECTED": "Desconectado",
"BTN_STATE_JOINING": "🚀 Entrando...",
"BTN_STATE_RECONNECTING": "🔄 Reconectando...",
"BTN_STATE_PLAYING": "▶ Reproduzindo...",
"BTN_STATE_PAUSING": "⏸ Pausando...",
"BTN_STATE_SYNCING_GROUP": "Sincronizando com o grupo ({time})...",
"BTN_STATE_SYNCING": "Sincronizando...",
"BTN_STATE_SYNCED": "✅ Sincronizado!",
"NOTIF_PLAY": "iniciou a reprodução",
"NOTIF_PAUSE": "pausou a reprodução",
"NOTIF_SEEK": "mudou a posição do vídeo",
"NOTIF_FORCE_PREPARE": "iniciou uma sincronização forçada",
"NOTIF_FORCE_EXECUTE": "sincronizou todos",
"DEBUG_NO_TAB": "Nenhuma aba alvo selecionada.",
"DEBUG_COMM_FAIL": "Não foi possível comunicar com o vídeo da aba.",
"EMPTY_PEERS_TITLE": "Nenhum participante ainda",
"EMPTY_PEERS_HINT": "Compartilhe seu link de convite para começar",
"EMPTY_HISTORY_TITLE": "Sem atividade ainda",
"EMPTY_HISTORY_HINT": "Reproduza, pause ou busque para ver o histórico",
"EMPTY_LOGS_TITLE": "Sem logs",
"EMPTY_LOGS_HINT": "Os eventos de conexão aparecerão aqui",
"EMPTY_ROOMS_TITLE": "Sem salas ativas",
"EMPTY_ROOMS_HINT": "Crie uma sala ou atualize para buscar salas públicas"
}
+177
View File
@@ -0,0 +1,177 @@
{
"LANG_CODE": "ru",
"HTML_CLASS": "lang-ru",
"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://ваш-сервер: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": "Обновление...",
"LABEL_ACTIVE_ROOM": "Активная комната",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Комната, к которой вы сейчас подключены",
"ACTIVE_ROOM_NONE": "НЕТ",
"ACTIVE_SERVER_OFFICIAL": "Официальный сервер",
"LABEL_INVITE_LINK": "Ссылка-приглашение",
"LABEL_INVITE_LINK_TOOLTIP": "Поделитесь этой ссылкой с друзьями, чтобы они вошли",
"LABEL_PEERS_IN_ROOM": "Участники в комнате",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Другие пользователи, подключенные к этой комнате",
"NO_PEERS_CONNECTED": "Нет подключенных участников",
"BTN_LEAVE_ROOM": "Выйти из комнаты",
"LABEL_SELECT_VIDEO": "Выбрать видео",
"LABEL_SELECT_VIDEO_TOOLTIP": "Выберите вкладку браузера, содержащую видео для синхронизации",
"OPTION_SELECT_TAB": "-- Выберите вкладку --",
"LABEL_REMOTE_CONTROL": "Дистанционное управление",
"BTN_COPY_INVITE": "📋 Ссылка",
"BTN_COPY_INVITE_TOOLTIP": "Копировать ссылку-приглашение",
"BTN_PLAY": "▶ Запуск",
"BTN_PLAY_TOOLTIP": "Отправить команду воспроизведения всем",
"BTN_PAUSE": "⏸ Пауза",
"BTN_PAUSE_TOOLTIP": "Отправить команду паузы всем",
"BTN_SYNC": "⚡ СИНХРО",
"BTN_SYNC_TOOLTIP": "Принудительно синхронизировать всех участников",
"OPTION_JUMP_TO_OTHERS": "Перейти к другим",
"OPTION_JUMP_TO_ME": "Синхронизировать по мне",
"OPTION_JUMP_MODE_TOOLTIP": "Выбрать цель синхронизации",
"LABEL_LAST_ACTIVITY": "Последняя активность",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Показывает самую последнюю команду воспроизведения, паузы или перемотки",
"NO_RECENT_COMMANDS": "Нет последних команд",
"LOBBY_HEADER": "ЛОББИ СЕРИИ",
"LOBBY_WAITING_FOR": "🎬 Ожидание: \"{title}\"",
"LOBBY_WAITING_PEERS": "Ожидание участников...",
"BTN_SKIP_PLAY": "Пропустить и запустить",
"BTN_SKIP_PLAY_TOOLTIP": "Закрыть лобби и воспроизвести в любом случае",
"LOBBY_CONNECT_FIRST": "Сначала войдите в комнату",
"LOBBY_CONNECT_FIRST_DESC": "Вам необходимо войти в комнату по ссылке-приглашению или создать новую, чтобы синхронизировать видео.",
"BTN_CREATE_ROOM_ALT": "Создать новую комнату",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Создать новую случайную комнату и войти",
"LABEL_USERNAME": "Ваше имя пользователя",
"LABEL_USERNAME_TOOLTIP": "Имя пользователя помогает другим узнать вас.",
"PLACEHOLDER_USERNAME": "Анонимный Коала",
"LABEL_HIDE_CLUTTER": "Чистый список вкладок",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Скрывает вкладки без видео и несвязанные домены для порядка",
"LABEL_AUTO_SYNC_NEXT": "Автосинхрон следующей серии",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Автоматически ставит на паузу и ждет всех при смене серии, затем запускает видео синхронно.",
"LABEL_AUTO_COPY_INVITE": "Автокопирование ссылки",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Автоматически копирует ссылку-приглашение в буфер обмена при создании новой комнаты.",
"LABEL_NOTIFICATIONS": "Уведомления браузера",
"LABEL_NOTIFICATIONS_TOOLTIP": "Показывает системные уведомления, когда кто-то входит/выходит или запускает/приостанавливает воспроизведение.",
"LABEL_LANGUAGE": "Язык приложения",
"LABEL_LANGUAGE_TOOLTIP": "Выберите предпочтительный язык интерфейса расширения",
"LABEL_TROUBLESHOOTING": "Устранение неполадок",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Инструменты для исправления проблем с подключением",
"BTN_REGEN_ID": "Сбросить Peer ID",
"BTN_REGEN_ID_TOOLTIP": "Сбросить ваш внутренний идентификатор и переподключиться",
"REGEN_ID_DESC": "Используйте при появлении ошибок дублирования идентификации.",
"LABEL_CONN_STATUS": "Статус подключения",
"LABEL_CONN_STATUS_TOOLTIP": "Текущий статус WebSocket-соединения с сервером",
"CONN_STATUS_DISCONNECTED": "Отключено",
"BTN_RETRY": "ПОВТОРИТЬ",
"BTN_RETRY_TOOLTIP": "Попробовать переподключиться к серверу",
"BTN_COPY_LOGS": "Копировать логи",
"BTN_COPY_LOGS_TOOLTIP": "Скопировать логи в буфер обмена для отправки",
"LABEL_VIDEO_DEBUG": "Отладочные данные видео",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Технические сведения о выбранном видеоэлементе",
"VIDEO_DEBUG_EMPTY": "Вкладка не выбрана или видео не обнаружено.",
"LABEL_HISTORY": "История действий",
"LABEL_HISTORY_TOOLTIP": "Хронологический список всех команд синхронизации в комнате",
"HISTORY_EMPTY": "Нет недавней активности",
"LABEL_LOGS": "Логи (Последние 50)",
"LABEL_LOGS_TOOLTIP": "Технические системные сообщения подключения",
"BTN_CLEAR": "ОЧИСТИТЬ",
"BTN_CLEAR_TOOLTIP": "Очистить вывод логов",
"LABEL_GITHUB": "Репозиторий GitHub",
"BTN_ONBOARDING_SKIP": "Пропустить",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Пропустить руководство",
"BTN_ONBOARDING_NEXT": "Далее",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Перейти к следующему шагу",
"ONBOARDING_1_TITLE": "Добро пожаловать в KoalaSync!",
"ONBOARDING_1_TEXT": "Смотрите видео в идеальной синхронизации вместе — где бы вы ни находились. Давайте проведем небольшую экскурсию!",
"ONBOARDING_2_TITLE": "1. Создайте комнату",
"ONBOARDING_2_TEXT": "Начните здесь. Создайте комнату и поделитесь ссылкой-приглашением со своими друзьями.",
"ONBOARDING_3_TITLE": "2. Выберите видео",
"ONBOARDING_3_TEXT": "Выберите вкладку с видео для синхронизации. Запуск, пауза, перемотка — воспроизведение синхронизируется у всех.",
"ONBOARDING_4_TITLE": "3. Настройте профиль",
"ONBOARDING_4_TEXT": "Введите имя пользователя, чтобы друзья знали, кто совершает действия.",
"ONBOARDING_5_TITLE": "Всё готово!",
"ONBOARDING_5_TEXT": "Время готовить попкорн. Приятного совместного просмотра!",
"ERR_CONN_TIMEOUT": "Время ожидания подключения истекло. Пожалуйста, попробуйте снова.",
"ERR_INVALID_SERVER_URL": "Неверный формат URL-адреса сервера.",
"ERR_IDENTITY_NOT_LOADED": "Идентификатор еще не загружен. Подождите немного и повторите попытку.",
"ERR_NO_PEERS_TIME": "Нет других участников с известной позицией плеера. Выберите 'Синхронизировать по мне'.",
"ERR_NO_VIDEO_TAB": "Не удалось подключиться к вкладке с видео.",
"ERR_SELECT_VIDEO": "Пожалуйста, сначала выберите вкладку с видео!",
"TOAST_INVITE_COPIED": "Ссылка скопирована в буфер обмена!",
"TOAST_COPY_FAILED": "Не удалось скопировать ссылку в буфер обмена",
"TOAST_LOBBY_SKIPPED": "Ожидание серии отменено.",
"TOAST_LOBBY_SKIP_FAILED": "Не удалось отменить ожидание серии.",
"TOAST_LOGS_COPIED": "Скопировано!",
"TOAST_PEER_JOINED": "{name} вошел в комнату",
"TOAST_PEER_LEFT": "{name} вышел из комнаты",
"TOAST_PEER_ACTION": "{name}: {action}",
"STATUS_CONNECTED": "Подключено",
"STATUS_RECONNECTING": "Переподключение...",
"STATUS_CONNECTING": "Подключение...",
"STATUS_FAILED": "Ошибка",
"STATUS_DISCONNECTED": "Отключено",
"BTN_STATE_JOINING": "🚀 Вход...",
"BTN_STATE_RECONNECTING": "🔄 Переподключение...",
"BTN_STATE_PLAYING": "▶ Запуск...",
"BTN_STATE_PAUSING": "⏸ Пауза...",
"BTN_STATE_SYNCING_GROUP": "Синхронизация по группе ({time})...",
"BTN_STATE_SYNCING": "Синхронизация...",
"BTN_STATE_SYNCED": "✅ Синхронно!",
"NOTIF_PLAY": "запустил воспроизведение",
"NOTIF_PAUSE": "приостановил воспроизведение",
"NOTIF_SEEK": "перемотал видео",
"NOTIF_FORCE_PREPARE": "запустил принудительный синхрон",
"NOTIF_FORCE_EXECUTE": "синхронизировал воспроизведение у всех",
"DEBUG_NO_TAB": "Целевая вкладка не выбрана.",
"DEBUG_COMM_FAIL": "Не удалось связаться с плеером на вкладке.",
"EMPTY_PEERS_TITLE": "Участников пока нет",
"EMPTY_PEERS_HINT": "Поделитесь ссылкой-приглашением, чтобы начать",
"EMPTY_HISTORY_TITLE": "История действий пуста",
"EMPTY_HISTORY_HINT": "Запускайте, останавливайте или перематывайте видео, чтобы увидеть лог",
"EMPTY_LOGS_TITLE": "Журнал пуст",
"EMPTY_LOGS_HINT": "События подключения будут отображаться здесь",
"EMPTY_ROOMS_TITLE": "Нет активных комнат",
"EMPTY_ROOMS_HINT": "Создайте комнату или обновите список, чтобы найти публичные"
}
+77 -67
View File
@@ -315,7 +315,7 @@
<body>
<div id="toast-container"></div>
<div class="header-row">
<h1><img src="icons/icon128.png" alt="KoalaSync Logo">KoalaSync</h1>
<h1 data-i18n="APP_TITLE"><img src="icons/icon128.png" alt="KoalaSync Logo">KoalaSync</h1>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="popup-version">
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
@@ -325,10 +325,10 @@
</div>
<div class="tabs">
<button class="tab-btn active" data-tab="tab-room" title="Room settings and connection">Room</button>
<button class="tab-btn" data-tab="tab-sync" title="Video sync controls and remote actions">Sync</button>
<button class="tab-btn" data-tab="tab-settings" title="Extension preferences">Settings</button>
<button class="tab-btn" data-tab="tab-dev" title="Advanced Diagnostics & Logs">Status</button>
<button class="tab-btn active" data-tab="tab-room" data-i18n="TAB_ROOM" data-i18n-title="TAB_ROOM_TOOLTIP" title="Room settings and connection">Room</button>
<button class="tab-btn" data-tab="tab-sync" data-i18n="TAB_SYNC" data-i18n-title="TAB_SYNC_TOOLTIP" title="Video sync controls and remote actions">Sync</button>
<button class="tab-btn" data-tab="tab-settings" data-i18n="TAB_SETTINGS" data-i18n-title="TAB_SETTINGS_TOOLTIP" title="Extension preferences">Settings</button>
<button class="tab-btn" data-tab="tab-dev" data-i18n="TAB_STATUS" data-i18n-title="TAB_STATUS_TOOLTIP" title="Advanced Diagnostics & Logs">Status</button>
</div>
<!-- Room Tab -->
@@ -336,35 +336,35 @@
<!-- JOIN SECTION: Visible when not in a room -->
<div id="section-join">
<button id="createRoomBtn" class="primary" style="margin-bottom: 16px; padding: 14px; font-size: 15px; background: linear-gradient(135deg, #6366f1, #a855f7); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);">+ Create New Room</button>
<button id="createRoomBtn" class="primary" style="margin-bottom: 16px; padding: 14px; font-size: 15px; background: linear-gradient(135deg, #6366f1, #a855f7); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);" data-i18n="BTN_CREATE_ROOM">+ Create New Room</button>
<details style="margin-bottom: 12px; background: var(--card); border: 1px solid #334155; border-radius: 8px; padding: 8px;">
<summary style="font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; cursor: pointer; outline: none;">Manual Connect / Advanced</summary>
<summary style="font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; cursor: pointer; outline: none;" data-i18n="MANUAL_CONNECT_HEADER">Manual Connect / Advanced</summary>
<div style="margin-top: 12px;">
<div class="form-group">
<label title="Select which KoalaSync server to use">Server</label>
<label title="Select which KoalaSync server to use" data-i18n="LABEL_SERVER">Server</label>
<div style="display:flex; gap:4px; margin-bottom:8px;">
<button id="serverOfficial" class="tab-btn active" style="flex:1; padding:6px; font-size:11px;" title="Use the official reliable server">Official</button>
<button id="serverCustom" class="tab-btn" style="flex:1; padding:6px; font-size:11px;" title="Connect to your own self-hosted server">Custom</button>
<button id="serverOfficial" class="tab-btn active" style="flex:1; padding:6px; font-size:11px;" data-i18n="BTN_SERVER_OFFICIAL" data-i18n-title="BTN_SERVER_OFFICIAL_TOOLTIP" title="Use the official reliable server">Official</button>
<button id="serverCustom" class="tab-btn" style="flex:1; padding:6px; font-size:11px;" data-i18n="BTN_SERVER_CUSTOM" data-i18n-title="BTN_SERVER_CUSTOM_TOOLTIP" title="Connect to your own self-hosted server">Custom</button>
</div>
<input type="text" id="serverUrl" placeholder="wss://your-server:3000" style="display:none;">
<input type="text" id="serverUrl" data-i18n-placeholder="PLACEHOLDER_SERVER_URL" placeholder="wss://your-server:3000" style="display:none;">
</div>
<div class="form-group">
<label title="The unique identifier for your sync room">Room ID</label>
<input type="text" id="roomId" placeholder="Enter Room ID" title="The unique ID of the room you want to join">
<label title="The unique identifier for your sync room" data-i18n="LABEL_ROOM_ID" data-i18n-title="LABEL_ROOM_ID_TOOLTIP">Room ID</label>
<input type="text" id="roomId" data-i18n-placeholder="PLACEHOLDER_ROOM_ID" data-i18n-title="PLACEHOLDER_ROOM_ID_TOOLTIP" placeholder="Enter Room ID" title="The unique ID of the room you want to join">
</div>
<div class="form-group">
<label title="Optional password to restrict room access">Password (Optional)</label>
<input type="password" id="password" placeholder="Room Password (optional)" title="Password for the room (leave empty if none)">
<label title="Optional password to restrict room access" data-i18n="LABEL_PASSWORD" data-i18n-title="LABEL_PASSWORD_TOOLTIP">Password (Optional)</label>
<input type="password" id="password" data-i18n-placeholder="PLACEHOLDER_PASSWORD" data-i18n-title="PLACEHOLDER_PASSWORD_TOOLTIP" placeholder="Room Password (optional)" title="Password for the room (leave empty if none)">
</div>
<div id="roomError" style="display:none; color:var(--error); font-size:11px; margin-bottom:8px; text-align:center;"></div>
<button id="joinBtn" class="primary" title="Connect to the room">Join Room</button>
<button id="joinBtn" class="primary" data-i18n="BTN_JOIN_ROOM" data-i18n-title="BTN_JOIN_ROOM_TOOLTIP" title="Connect to the room">Join Room</button>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1.5rem; margin-bottom: 8px;">
<label style="margin:0;" title="List of publicly available rooms on this server">Public Rooms</label>
<button id="refreshRooms" class="secondary" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;" title="Refresh the list of public rooms">REFRESH</button>
<label style="margin:0;" title="List of publicly available rooms on this server" data-i18n="LABEL_PUBLIC_ROOMS" data-i18n-title="LABEL_PUBLIC_ROOMS_TOOLTIP">Public Rooms</label>
<button id="refreshRooms" class="secondary" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;" data-i18n="BTN_REFRESH" data-i18n-title="BTN_REFRESH_TOOLTIP" title="Refresh the list of public rooms">REFRESH</button>
</div>
<div id="publicRooms" class="info-card" style="max-height: 120px; overflow-y: auto; padding: 4px;">
<div style="text-align:center; color: var(--text-muted); font-size: 11px; padding: 10px;">Refreshing...</div>
<div style="text-align:center; color: var(--text-muted); font-size: 11px; padding: 10px;" data-i18n="PUBLIC_ROOMS_REFRESHING">Refreshing...</div>
</div>
</div>
</details>
@@ -374,14 +374,14 @@
<div id="section-active" style="display:none;">
<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">Active Room</label>
<div id="activeRoomId" style="font-weight: 700; color: var(--accent); font-size: 16px; letter-spacing: 1px;">NONE</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>
<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 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>
<div class="info-card" style="margin-bottom: 20px;">
<label title="Share this link with friends so they can join">Invite Link</label>
<label title="Share this link with friends so they can join" data-i18n="LABEL_INVITE_LINK" data-i18n-title="LABEL_INVITE_LINK_TOOLTIP">Invite Link</label>
<div class="invite-box">
<input type="text" id="inviteLink" readonly>
<button id="copyInvite" class="secondary">📋</button>
@@ -389,13 +389,13 @@
</div>
<div style="margin-bottom: 20px;">
<label title="Other users currently connected to this room">Peers in Room</label>
<label title="Other users currently connected to this room" data-i18n="LABEL_PEERS_IN_ROOM" data-i18n-title="LABEL_PEERS_IN_ROOM_TOOLTIP">Peers in Room</label>
<div id="peerList" class="info-card">
<div style="text-align:center; color: var(--text-muted); font-size: 12px;">No peers connected</div>
<div style="text-align:center; color: var(--text-muted); font-size: 12px;" data-i18n="NO_PEERS_CONNECTED">No peers connected</div>
</div>
</div>
<button id="leaveBtn" class="secondary" style="color: var(--error); border-color: var(--error);">Leave Room</button>
<button id="leaveBtn" class="secondary" style="color: var(--error); border-color: var(--error);" data-i18n="BTN_LEAVE_ROOM">Leave Room</button>
</div>
</div>
@@ -404,44 +404,44 @@
<!-- SYNC ACTIVE: Visible when in a room -->
<div id="sync-active">
<div class="form-group">
<label title="Choose the browser tab containing the video to sync">Select Video</label>
<label title="Choose the browser tab containing the video to sync" data-i18n="LABEL_SELECT_VIDEO" data-i18n-title="LABEL_SELECT_VIDEO_TOOLTIP">Select Video</label>
<select id="targetTab">
<option value="">-- Select a Tab --</option>
<option value="" data-i18n="OPTION_SELECT_TAB">-- Select a Tab --</option>
</select>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label style="margin: 0;">Remote Control</label>
<button id="syncTabCopyInvite" title="Copy Invite Link" style="background:transparent; border: 1px solid #334155; border-radius: 6px; padding: 4px 8px; font-size: 11px; cursor:pointer; opacity:0.8; transition: opacity 0.2s; color: var(--text-muted); display: flex; align-items: center; gap: 4px; white-space: nowrap;">📋 Invite Link</button>
<label style="margin: 0;" data-i18n="LABEL_REMOTE_CONTROL">Remote Control</label>
<button id="syncTabCopyInvite" title="Copy Invite Link" style="background:transparent; border: 1px solid #334155; border-radius: 6px; padding: 4px 8px; font-size: 11px; cursor:pointer; opacity:0.8; transition: opacity 0.2s; color: var(--text-muted); display: flex; align-items: center; gap: 4px; white-space: nowrap;" data-i18n="BTN_COPY_INVITE" data-i18n-title="BTN_COPY_INVITE_TOOLTIP">📋 Invite Link</button>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
<button id="playBtn" class="primary" style="flex:1; background: var(--success);" title="Send a Play command to everyone">▶ Play</button>
<button id="pauseBtn" class="primary" style="flex:1; background: var(--error);" title="Send a Pause command to everyone">⏸ Pause</button>
<button id="playBtn" class="primary" style="flex:1; background: var(--success);" title="Send a Play command to everyone" data-i18n="BTN_PLAY" data-i18n-title="BTN_PLAY_TOOLTIP">▶ Play</button>
<button id="pauseBtn" class="primary" style="flex:1; background: var(--error);" title="Send a Pause command to everyone" data-i18n="BTN_PAUSE" data-i18n-title="BTN_PAUSE_TOOLTIP">⏸ Pause</button>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 15px; align-items: stretch;">
<button id="forceSyncBtn" class="primary" style="background: linear-gradient(135deg, #6366f1, #a855f7); flex: 1;" title="Force all users to sync up">⚡ SYNC</button>
<select id="forceSyncMode" style="flex: 1; background: var(--card); border: 1px solid #334155; color: white; padding: 10px 8px; border-radius: 8px; font-size: 11px; font-family: inherit; cursor: pointer; align-self: stretch;" title="Choose sync target">
<option value="jump-to-others">Jump to Others</option>
<option value="jump-to-me">Jump to Me</option>
<button id="forceSyncBtn" class="primary" style="background: linear-gradient(135deg, #6366f1, #a855f7); flex: 1;" title="Force all users to sync up" data-i18n="BTN_SYNC" data-i18n-title="BTN_SYNC_TOOLTIP">⚡ SYNC</button>
<select id="forceSyncMode" style="flex: 1; background: var(--card); border: 1px solid #334155; color: white; padding: 10px 8px; border-radius: 8px; font-size: 11px; font-family: inherit; cursor: pointer; align-self: stretch;" title="Choose sync target" data-i18n-title="OPTION_JUMP_MODE_TOOLTIP">
<option value="jump-to-others" data-i18n="OPTION_JUMP_TO_OTHERS">Jump to Others</option>
<option value="jump-to-me" data-i18n="OPTION_JUMP_TO_ME">Jump to Me</option>
</select>
</div>
<!-- NEW: Last Action Status Card -->
<label title="Shows the most recent play, pause, or seek command">Last Activity Status</label>
<label title="Shows the most recent play, pause, or seek command" data-i18n="LABEL_LAST_ACTIVITY" data-i18n-title="LABEL_LAST_ACTIVITY_TOOLTIP">Last Activity Status</label>
<div id="lastActionCard" class="info-card" style="margin-bottom: 15px; max-height: 120px; overflow-y: auto;">
<div style="text-align:center; color: var(--text-muted); font-size: 10px;">No recent commands</div>
<div style="text-align:center; color: var(--text-muted); font-size: 10px;" data-i18n="NO_RECENT_COMMANDS">No recent commands</div>
</div>
<!-- Episode Auto-Sync Lobby Status -->
<div id="episodeLobbyCard" class="info-card" style="display:none; margin-bottom: 15px; border-left: 4px solid var(--star); animation: fadeIn 0.3s ease-out;">
<div style="display:flex; align-items:center; gap: 6px; margin-bottom: 6px;">
<span style="font-size: 16px;"></span>
<span style="font-weight: 700; color: var(--star); font-size: 12px;">EPISODE LOBBY</span>
<span style="font-weight: 700; color: var(--star); font-size: 12px;" data-i18n="LOBBY_HEADER">EPISODE LOBBY</span>
</div>
<div id="lobbyTitle" style="font-size: 11px; color: var(--text); margin-bottom: 6px; font-weight: 600;"></div>
<div id="lobbyPeerStatus" style="font-size: 10px; color: var(--text-muted); margin-bottom: 8px;"></div>
<button id="cancelLobbyBtn" class="secondary" style="margin-top: 4px; padding: 6px 10px; font-size: 11px; width: auto; display: block;" title="Cancel lobby and play anyway">Skip & Play anyway</button>
<button id="cancelLobbyBtn" class="secondary" style="margin-top: 4px; padding: 6px 10px; font-size: 11px; width: auto; display: block;" title="Cancel lobby and play anyway" data-i18n="BTN_SKIP_PLAY" data-i18n-title="BTN_SKIP_PLAY_TOOLTIP">Skip & Play anyway</button>
</div>
<div id="peerListSync" class="info-card" style="display:none;"></div>
@@ -450,21 +450,21 @@
<!-- SYNC INACTIVE: Visible when not in a room -->
<div id="sync-inactive" style="display:none; text-align: center; padding: 20px 10px;">
<div style="font-size: 32px; margin-bottom: 12px;">🔒</div>
<h3 style="margin: 0 0 8px 0; color: var(--accent); font-size: 15px;">Connect to a room first</h3>
<p style="color: var(--text-muted); font-size: 12px; margin-bottom: 20px;">You need to join a room via an invite link or create a new one to sync videos.</p>
<button id="syncTabCreateRoomBtn" class="primary" style="padding: 12px; font-size: 14px; background: linear-gradient(135deg, #6366f1, #a855f7); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);" title="Create a new random room and join it">Create New Room</button>
<h3 style="margin: 0 0 8px 0; color: var(--accent); font-size: 15px;" data-i18n="LOBBY_CONNECT_FIRST">Connect to a room first</h3>
<p style="color: var(--text-muted); font-size: 12px; margin-bottom: 20px;" data-i18n="LOBBY_CONNECT_FIRST_DESC">You need to join a room via an invite link or create a new one to sync videos.</p>
<button id="syncTabCreateRoomBtn" class="primary" style="padding: 12px; font-size: 14px; background: linear-gradient(135deg, #6366f1, #a855f7); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);" title="Create a new random room and join it" data-i18n="BTN_CREATE_ROOM_ALT" data-i18n-title="BTN_CREATE_ROOM_ALT_TOOLTIP">Create New Room</button>
</div>
</div>
<!-- Settings Tab -->
<div id="tab-settings" class="tab-content">
<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;" title="Username helps others identify you.">Your Username</label>
<input type="text" id="username" placeholder="Anonymous Koala" maxlength="20" style="width: 150px;">
<label style="margin-bottom: 0;" title="Username helps others identify you." data-i18n="LABEL_USERNAME" data-i18n-title="LABEL_USERNAME_TOOLTIP">Your Username</label>
<input type="text" id="username" data-i18n-placeholder="PLACEHOLDER_USERNAME" placeholder="Anonymous Koala" maxlength="20" style="width: 150px;">
</div>
<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; cursor: help;" title="Filters out non-video tabs and unrelated domains to keep the list clean">Hide Clutter Tabs</label>
<label style="margin-bottom: 0; cursor: help;" title="Filters out non-video tabs and unrelated domains to keep the list clean" data-i18n="LABEL_HIDE_CLUTTER" data-i18n-title="LABEL_HIDE_CLUTTER_TOOLTIP">Hide Clutter Tabs</label>
<label class="toggle-switch">
<input type="checkbox" id="filterNoise" checked>
<span class="slider"></span>
@@ -472,7 +472,7 @@
</div>
<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; cursor: help;" title="Pauses automatically and waits for all peers when an episode changes, then sync-starts together.">Auto-Sync Next Episode</label>
<label style="margin-bottom: 0; cursor: help;" title="Pauses automatically and waits for all peers when an episode changes, then sync-starts together." data-i18n="LABEL_AUTO_SYNC_NEXT" data-i18n-title="LABEL_AUTO_SYNC_NEXT_TOOLTIP">Auto-Sync Next Episode</label>
<label class="toggle-switch">
<input type="checkbox" id="autoSyncNextEpisode">
<span class="slider"></span>
@@ -480,7 +480,7 @@
</div>
<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; cursor: help;" title="Automatically copies the invite link to your clipboard when creating a new room.">Auto-Copy Invite Link</label>
<label style="margin-bottom: 0; cursor: help;" title="Automatically copies the invite link to your clipboard when creating a new room." data-i18n="LABEL_AUTO_COPY_INVITE" data-i18n-title="LABEL_AUTO_COPY_INVITE_TOOLTIP">Auto-Copy Invite Link</label>
<label class="toggle-switch">
<input type="checkbox" id="autoCopyInvite" checked>
<span class="slider"></span>
@@ -488,52 +488,62 @@
</div>
<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; cursor: help;" title="Shows native system notifications when someone joins/leaves or plays/pauses.">Browser Notifications</label>
<label style="margin-bottom: 0; cursor: help;" title="Shows native system notifications when someone joins/leaves or plays/pauses." data-i18n="LABEL_NOTIFICATIONS" data-i18n-title="LABEL_NOTIFICATIONS_TOOLTIP">Browser Notifications</label>
<label class="toggle-switch">
<input type="checkbox" id="browserNotifications">
<span class="slider"></span>
</label>
</div>
<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>
</div>
<div style="margin-top: 15px; padding: 8px; border-top: 1px solid var(--card);">
<label title="Tools for fixing connection issues">Troubleshooting</label>
<button id="regenId" class="secondary" style="width: 100%; font-size: 11px;" title="Regenerate your internal ID and reconnect">Regenerate Peer ID</button>
<p style="font-size: 9px; color: var(--text-muted); margin-top: 5px; text-align: center;">Use this if you see "Duplicate Identity" errors.</p>
<label title="Tools for fixing connection issues" data-i18n="LABEL_TROUBLESHOOTING" data-i18n-title="LABEL_TROUBLESHOOTING_TOOLTIP">Troubleshooting</label>
<button id="regenId" class="secondary" style="width: 100%; font-size: 11px;" title="Regenerate your internal ID and reconnect" data-i18n="BTN_REGEN_ID" data-i18n-title="BTN_REGEN_ID_TOOLTIP">Regenerate Peer ID</button>
<p style="font-size: 9px; color: var(--text-muted); margin-top: 5px; text-align: center;" data-i18n="REGEN_ID_DESC">Use this if you see "Duplicate Identity" errors.</p>
</div>
</div>
<!-- Dev Tab -->
<div id="tab-dev" class="tab-content">
<label title="Current WebSocket connection state">Connection Status</label>
<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;">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">RETRY</button>
<button id="copyLogs" class="btn secondary" style="width: auto; padding: 4px 10px; font-size: 11px;" title="Copy logs to clipboard for sharing">Copy Logs</button>
<span id="connText" style="flex:1;" data-i18n="CONN_STATUS_DISCONNECTED">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">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;">
<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">
No tab selected or video detected.
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;">
<label title="Chronological log of all sync commands in the room">Full Action History</label>
<label title="Chronological log of all sync commands in the room" data-i18n="LABEL_HISTORY" data-i18n-title="LABEL_HISTORY_TOOLTIP">Full Action History</label>
</div>
<div id="historyList" class="info-card" style="max-height: 120px; overflow-y: auto; font-size: 10px; margin-bottom: 15px;">
<div style="text-align:center; color: var(--text-muted); font-size: 11px;">No activity yet</div>
<div style="text-align:center; color: var(--text-muted); font-size: 11px;" data-i18n="HISTORY_EMPTY">No activity yet</div>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;">
<label title="Technical connection logs for debugging">Logs (Last 50)</label>
<button id="clearLogs" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;" title="Clear log output">CLEAR</button>
<label title="Technical connection logs for debugging" data-i18n="LABEL_LOGS" data-i18n-title="LABEL_LOGS_TOOLTIP">Logs (Last 50)</label>
<button id="clearLogs" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;" title="Clear log output" data-i18n="BTN_CLEAR" data-i18n-title="BTN_CLEAR_TOOLTIP">CLEAR</button>
</div>
<div id="logList"></div>
<div style="margin-top: 20px; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px;">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="color: var(--text-muted); text-decoration: none; font-size: 10px; opacity: 0.6; display: block;">GitHub Repository</a>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="color: var(--text-muted); text-decoration: none; font-size: 10px; opacity: 0.6; display: block;" data-i18n="LABEL_GITHUB">GitHub Repository</a>
<div id="appVersion" style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;">v0.0.0</div>
</div>
</div>
@@ -543,12 +553,12 @@
<!-- Onboarding Overlay -->
<div id="onboarding-overlay" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); backdrop-filter: blur(2px); z-index:1000; align-items:flex-end; justify-content:center; padding-bottom: 20px;">
<div id="onboarding-card" style="background:var(--card); padding:24px; border-radius:16px; max-width:280px; width:90%; text-align:center; box-shadow:0 8px 32px rgba(0,0,0,0.5); ">
<div id="onboarding-icon" style="font-size:48px; margin-bottom:12px;">\u{1F44B}</div>
<div id="onboarding-icon" style="font-size:48px; margin-bottom:12px;">👋</div>
<h2 id="onboarding-title" style="color:var(--accent); margin:0 0 8px; font-size:16px;">Welcome to KoalaSync!</h2>
<p id="onboarding-text" style="color:var(--text-muted); font-size:13px; margin:0 0 16px; line-height:1.4;">Let's get you started.</p>
<div style="display:flex; gap:8px; justify-content:center;">
<button id="onboarding-skip" class="secondary" style="width:auto; padding:8px 16px;" title="Skip the tutorial">Skip</button>
<button id="onboarding-next" class="primary" style="width:auto; padding:8px 16px;" title="Go to next step">Next</button>
<button id="onboarding-skip" class="secondary" style="width:auto; padding:8px 16px;" title="Skip the tutorial" data-i18n="BTN_ONBOARDING_SKIP" data-i18n-title="BTN_ONBOARDING_SKIP_TOOLTIP">Skip</button>
<button id="onboarding-next" class="primary" style="width:auto; padding:8px 16px;" title="Go to next step" data-i18n="BTN_ONBOARDING_NEXT" data-i18n-title="BTN_ONBOARDING_NEXT_TOOLTIP">Next</button>
</div>
<div id="onboarding-dots" style="margin-top:12px; display:flex; gap:6px; justify-content:center;"></div>
</div>
+70 -43
View File
@@ -1,6 +1,7 @@
import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js';
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
import { getAvatarForName, generateUsername } from './shared/names.js';
import { loadLocale, translateDOM, getMessage } from './i18n.js';
const elements = {
@@ -50,7 +51,8 @@ const elements = {
browserNotifications: document.getElementById('browserNotifications'),
autoCopyInvite: document.getElementById('autoCopyInvite'),
syncTabCopyInvite: document.getElementById('syncTabCopyInvite'),
cancelLobbyBtn: document.getElementById('cancelLobbyBtn')
cancelLobbyBtn: document.getElementById('cancelLobbyBtn'),
langSelector: document.getElementById('langSelector')
};
let localPeerId = null;
@@ -69,7 +71,20 @@ let forceSyncDone = false;
// --- Initialization ---
async function init() {
// Load Settings
const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'filterNoise', 'username', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite']);
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';
chrome.storage.sync.set({ locale: activeLang });
}
await loadLocale(activeLang);
translateDOM();
if (elements.langSelector) elements.langSelector.value = activeLang;
let username = data.username;
if (!username) {
username = generateUsername();
@@ -195,7 +210,7 @@ function updateLastActionUI(state, peers) {
elements.lastActionCard.replaceChildren();
const el = document.createElement('div');
el.style.cssText = 'text-align:center; color: var(--text-muted); font-size: 10px;';
el.textContent = 'No recent commands';
el.textContent = getMessage('NO_RECENT_COMMANDS');
elements.lastActionCard.appendChild(el);
return;
}
@@ -204,14 +219,14 @@ function updateLastActionUI(state, peers) {
const safeAcks = state.acks || [];
const actionNames = {
'play': 'PLAY',
'pause': 'PAUSE',
'seek': 'SEEK',
'force_sync_prepare': 'SYNCING...',
'force_sync_execute': 'FORCE PLAY'
'play': getMessage('BTN_PLAY').replace('▶ ', '').toUpperCase(),
'pause': getMessage('BTN_PAUSE').replace('⏸ ', '').toUpperCase(),
'seek': getMessage('NOTIF_SEEK').toUpperCase(),
'force_sync_prepare': getMessage('BTN_STATE_SYNCING').toUpperCase(),
'force_sync_execute': getMessage('BTN_STATE_SYNCED').toUpperCase()
};
let senderName = state.senderId === 'You' ? 'You' : state.senderId;
let senderName = state.senderId === 'You' ? (getMessage('LABEL_YOU') || 'YOU') : state.senderId;
const senderPeer = safePeers.find(p => (p.peerId || p) === state.senderId);
if (senderPeer && senderPeer.username) senderName = senderPeer.username;
@@ -326,10 +341,10 @@ function startInterpolation() {
function renderEmpty(container, type) {
const states = {
peers: { icon: '\u{1F465}', title: 'No peers yet', hint: 'Share your invite link to get started' },
history: { icon: '\u{1F4CB}', title: 'No activity yet', hint: 'Play, pause, or seek to see history' },
logs: { icon: '\u{1F4DD}', title: 'No logs', hint: 'Connection events will appear here' },
rooms: { icon: '\u{1F50D}', title: 'No active rooms', hint: 'Create a room or refresh to find public ones' }
peers: { icon: '\u{1F465}', title: getMessage('EMPTY_PEERS_TITLE'), hint: getMessage('EMPTY_PEERS_HINT') },
history: { icon: '\u{1F4CB}', title: getMessage('EMPTY_HISTORY_TITLE'), hint: getMessage('EMPTY_HISTORY_HINT') },
logs: { icon: '\u{1F4DD}', title: getMessage('EMPTY_LOGS_TITLE'), hint: getMessage('EMPTY_LOGS_HINT') },
rooms: { icon: '\u{1F50D}', title: getMessage('EMPTY_ROOMS_TITLE'), hint: getMessage('EMPTY_ROOMS_HINT') }
};
const state = states[type] || { icon: '', title: '', hint: '' };
const wrapper = document.createElement('div');
@@ -423,7 +438,7 @@ function updatePeerList(peers) {
if (pId === localPeerId) {
const you = document.createElement('span');
you.style.cssText = 'font-size:10px; color:var(--accent); font-weight:bold;';
you.textContent = 'YOU';
you.textContent = getMessage('LABEL_YOU') || 'YOU';
header.appendChild(you);
}
@@ -498,7 +513,7 @@ function detectPeerChanges(newPeers) {
const id = peer.peerId || peer;
if (!oldIds.has(id)) {
const name = peer.username || id.substring(0, 4);
showToast(`${name} joined the room`, 'success');
showToast(getMessage('TOAST_PEER_JOINED', { name }), 'success');
}
}
@@ -506,7 +521,7 @@ function detectPeerChanges(newPeers) {
const id = oldPeer.peerId || oldPeer;
if (!newIds.has(id)) {
const name = oldPeer.username || id.substring(0, 4);
showToast(`${name} left the room`, 'info');
showToast(getMessage('TOAST_PEER_LEFT', { name }), 'info');
}
}
@@ -677,7 +692,7 @@ function applyConnectionStatus(status) {
}
if (elements.connText) {
elements.connText.textContent = connected ? 'Connected' : (reconnecting ? 'Reconnecting...' : (connecting ? 'Connecting...' : (failed ? 'Failed' : 'Disconnected')));
elements.connText.textContent = connected ? getMessage('STATUS_CONNECTED') : (reconnecting ? getMessage('STATUS_RECONNECTING') : (connecting ? getMessage('STATUS_CONNECTING') : (failed ? getMessage('STATUS_FAILED') : getMessage('STATUS_DISCONNECTED'))));
}
if (elements.retryBtn) {
elements.retryBtn.style.display = failed ? 'block' : 'none';
@@ -686,16 +701,16 @@ function applyConnectionStatus(status) {
if (elements.joinBtn) {
if (connecting || reconnecting) {
elements.joinBtn.disabled = true;
elements.joinBtn.textContent = connecting ? '🚀 Joining...' : '🔄 Reconnecting...';
elements.joinBtn.textContent = connecting ? getMessage('BTN_STATE_JOINING') : getMessage('BTN_STATE_RECONNECTING');
} else {
elements.joinBtn.disabled = false;
elements.joinBtn.textContent = 'Join Room';
elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM');
}
}
if (elements.playBtn) elements.playBtn.textContent = '▶ Play';
if (elements.pauseBtn) elements.pauseBtn.textContent = '⏸ Pause';
if (elements.forceSyncBtn) elements.forceSyncBtn.textContent = '⚡ SYNC';
if (elements.playBtn) elements.playBtn.textContent = getMessage('BTN_PLAY');
if (elements.pauseBtn) elements.pauseBtn.textContent = getMessage('BTN_PAUSE');
if (elements.forceSyncBtn) elements.forceSyncBtn.textContent = getMessage('BTN_SYNC');
}
function updateHistory(history) {
@@ -885,6 +900,18 @@ elements.username.addEventListener('change', () => {
chrome.storage.sync.set({ username: elements.username.value });
});
if (elements.langSelector) {
elements.langSelector.addEventListener('change', async () => {
const selectedLang = elements.langSelector.value;
await chrome.storage.sync.set({ locale: selectedLang });
await loadLocale(selectedLang);
translateDOM();
refreshLogs();
refreshHistory();
populateTabs();
});
}
elements.serverUrl.addEventListener('change', () => {
let url = elements.serverUrl.value.trim();
if (url && !url.includes('://')) {
@@ -1190,14 +1217,14 @@ elements.copyInvite.addEventListener('click', () => {
elements.copyInvite.textContent = '✓';
elements.copyInvite.style.background = 'var(--success)';
elements.copyInvite.style.color = 'white';
showToast('Invite link copied!', 'success', 2000);
showToast(getMessage('TOAST_INVITE_COPIED'), 'success', 2000);
setTimeout(() => {
elements.copyInvite.textContent = original;
elements.copyInvite.style.background = '';
elements.copyInvite.style.color = '';
}, 2000);
}).catch(() => {
showToast('Failed to copy to clipboard', 'error');
showToast(getMessage('TOAST_COPY_FAILED'), 'error');
});
});
@@ -1207,7 +1234,7 @@ if (elements.syncTabCopyInvite) {
const original = elements.syncTabCopyInvite.textContent;
elements.syncTabCopyInvite.textContent = '✓';
elements.syncTabCopyInvite.style.color = 'var(--success)';
showToast('Invite link copied!', 'success', 2000);
showToast(getMessage('TOAST_INVITE_COPIED'), 'success', 2000);
setTimeout(() => {
elements.syncTabCopyInvite.textContent = original;
elements.syncTabCopyInvite.style.color = '';
@@ -1220,12 +1247,12 @@ if (elements.cancelLobbyBtn) {
elements.cancelLobbyBtn.addEventListener('click', () => {
chrome.runtime.sendMessage({ type: 'CANCEL_EPISODE_LOBBY' }, (response) => {
if (response && response.status === 'ok') {
showToast('Episode Lobby skipped.', 'info');
showToast(getMessage('TOAST_LOBBY_SKIPPED'), 'info');
if (elements.episodeLobbyCard) {
elements.episodeLobbyCard.style.display = 'none';
}
} else {
showToast('Failed to skip lobby.', 'error');
showToast(getMessage('TOAST_LOBBY_SKIP_FAILED'), 'error');
}
});
});
@@ -1261,14 +1288,14 @@ chrome.runtime.onMessage.addListener((msg) => {
const state = msg.state;
if (state && state.senderId && state.senderId !== 'You') {
const actionNames = {
'play': '▶ Play',
'pause': '⏸ Pause',
'seek': '⏩ Seek',
'force_sync_prepare': '⚡ Sync',
'force_sync_execute': '⚡ Sync Play'
'play': getMessage('BTN_PLAY'),
'pause': getMessage('BTN_PAUSE'),
'seek': getMessage('NOTIF_SEEK'),
'force_sync_prepare': getMessage('BTN_STATE_SYNCING'),
'force_sync_execute': getMessage('BTN_STATE_SYNCED')
};
const action = actionNames[state.action] || state.action;
showToast(`${state.senderId} ${action}`, 'info', 2000);
showToast(getMessage('TOAST_PEER_ACTION', { name: state.senderId, action }), 'info', 2000);
}
if (state && (state.action === 'play' || state.action === 'pause')) {
@@ -1277,10 +1304,10 @@ chrome.runtime.onMessage.addListener((msg) => {
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
const peerCount = res && res.peers ? res.peers.length : 1;
if (state.acks && state.acks.length >= peerCount) {
btn.textContent = '✅ Synced!';
btn.textContent = getMessage('BTN_STATE_SYNCED');
setTimeout(() => {
btn.disabled = false;
btn.textContent = state.action === 'play' ? '▶ Play' : '⏸ Pause';
btn.textContent = state.action === 'play' ? getMessage('BTN_PLAY') : getMessage('BTN_PAUSE');
}, 2000);
}
});
@@ -1295,9 +1322,9 @@ chrome.runtime.onMessage.addListener((msg) => {
}
if (elements.forceSyncBtn) {
elements.forceSyncBtn.disabled = false;
elements.forceSyncBtn.textContent = '✅ Synced!';
elements.forceSyncBtn.textContent = getMessage('BTN_STATE_SYNCED');
setTimeout(() => {
elements.forceSyncBtn.textContent = '⚡ SYNC';
elements.forceSyncBtn.textContent = getMessage('BTN_SYNC');
}, 2000);
}
}
@@ -1508,11 +1535,11 @@ function updateLobbyUI(lobby, peers) {
// --- Onboarding Tour ---
const onboardingSteps = [
{ icon: '👋', title: 'Welcome to KoalaSync!', text: 'Watch videos together in perfect sync — no matter where you are. Let\'s take a quick tour!', targetTab: 'tab-room' },
{ icon: '🏠', title: '1. Create a Room', text: 'Start here. Create a room and share the invite link with your friends.', targetTab: 'tab-room' },
{ icon: '🎬', title: '2. Select Video', text: 'Navigate here to select the video you want to sync. Play, pause, and seek — everyone stays in sync.', targetTab: 'tab-sync' },
{ icon: '⚙️', title: '3. Personalize', text: 'Pick a fun username so your friends know who you are.', targetTab: 'tab-settings' },
{ icon: '🎉', title: 'You\'re all set!', text: 'Time to grab some popcorn. Enjoy watching together!', targetTab: 'tab-room' }
{ icon: '👋', get title() { return getMessage('ONBOARDING_1_TITLE'); }, get text() { return getMessage('ONBOARDING_1_TEXT'); }, targetTab: 'tab-room' },
{ icon: '🏠', get title() { return getMessage('ONBOARDING_2_TITLE'); }, get text() { return getMessage('ONBOARDING_2_TEXT'); }, targetTab: 'tab-room' },
{ icon: '🎬', get title() { return getMessage('ONBOARDING_3_TITLE'); }, get text() { return getMessage('ONBOARDING_3_TEXT'); }, targetTab: 'tab-sync' },
{ icon: '⚙️', get title() { return getMessage('ONBOARDING_4_TITLE'); }, get text() { return getMessage('ONBOARDING_4_TEXT'); }, targetTab: 'tab-settings' },
{ icon: '🎉', get title() { return getMessage('ONBOARDING_5_TITLE'); }, get text() { return getMessage('ONBOARDING_5_TEXT'); }, targetTab: 'tab-room' }
];
let onboardingStep = 0;
@@ -1562,7 +1589,7 @@ function renderOnboardingStep() {
dots.appendChild(dot);
});
nextBtn.textContent = onboardingStep === onboardingSteps.length - 1 ? 'Done!' : 'Next';
nextBtn.textContent = onboardingStep === onboardingSteps.length - 1 ? (getMessage('ONBOARDING_DONE') !== 'ONBOARDING_DONE' ? getMessage('ONBOARDING_DONE') : 'Done!') : getMessage('BTN_ONBOARDING_NEXT');
}
function completeOnboarding() {
+55
View File
@@ -0,0 +1,55 @@
const fs = require('fs');
const path = require('path');
const localesDir = path.join(__dirname, '..', 'extension', 'locales');
const enPath = path.join(localesDir, 'en.json');
if (!fs.existsSync(enPath)) {
console.error('CRITICAL: en.json is missing!');
process.exit(1);
}
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) {
const filePath = path.join(localesDir, file);
try {
const dict = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const keys = Object.keys(dict);
const missingKeys = enKeys.filter(k => !keys.includes(k));
const extraKeys = keys.filter(k => !enKeys.includes(k));
if (missingKeys.length > 0 || extraKeys.length > 0) {
hasError = true;
console.error(`${file} has inconsistencies:`);
if (missingKeys.length > 0) {
console.error(` Missing keys (${missingKeys.length}):`, missingKeys);
}
if (extraKeys.length > 0) {
console.error(` Extra keys (${extraKeys.length}):`, extraKeys);
}
} else {
console.log(`${file} is fully consistent (matches all keys).`);
}
} catch (err) {
hasError = true;
console.error(`❌ Failed to parse ${file}:`, err.message);
}
}
console.log('');
if (hasError) {
console.error('❌ Locale consistency check failed! Please fix the errors listed above.');
process.exit(1);
} else {
console.log('🎉 All locale files are perfectly synchronized and consistent!');
process.exit(0);
}