Created
February 8, 2026 13:40
-
-
Save adolenc/09cc70bc9ed0cb14d656d88dc57dfa07 to your computer and use it in GitHub Desktop.
Userscript for toggling between KR and EN subtitles on youtube
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name YouTube Subtitle Toggle (EN/KR) | |
| // @namespace https://example.com/ | |
| // @version 0.1 | |
| // @description Toggle English and Korean subtitles on YouTube with the "s" key. | |
| // @match https://www.youtube.com/* | |
| // @match https://m.youtube.com/* | |
| // @match https://youtu.be/* | |
| // @run-at document-end | |
| // @grant none | |
| // ==/UserScript== | |
| (() => { | |
| 'use strict'; | |
| const TOGGLE_KEY = 's'; | |
| const TOGGLE_DEBOUNCE_MS = 300; | |
| const TOAST_ID = 'yt-subtitle-toggle-toast'; | |
| const TOAST_HIDE_MS = 2000; | |
| let lastToggleAt = 0; | |
| let toastTimer = null; | |
| function isEditableTarget(target) { | |
| if (!target || !(target instanceof Element)) { | |
| return false; | |
| } | |
| const tagName = target.tagName; | |
| if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') { | |
| return true; | |
| } | |
| if (target.isContentEditable) { | |
| return true; | |
| } | |
| return Boolean(target.closest('[contenteditable="true"]')); | |
| } | |
| function showToast(message) { | |
| let toast = document.getElementById(TOAST_ID); | |
| if (!toast) { | |
| toast = document.createElement('div'); | |
| toast.id = TOAST_ID; | |
| toast.style.position = 'fixed'; | |
| toast.style.bottom = '20px'; | |
| toast.style.left = '50%'; | |
| toast.style.transform = 'translateX(-50%)'; | |
| toast.style.background = 'rgba(0, 0, 0, 0.8)'; | |
| toast.style.color = '#fff'; | |
| toast.style.padding = '8px 12px'; | |
| toast.style.borderRadius = '4px'; | |
| toast.style.fontSize = '14px'; | |
| toast.style.zIndex = '2147483647'; | |
| toast.style.transition = 'opacity 0.2s ease'; | |
| toast.style.opacity = '0'; | |
| document.documentElement.appendChild(toast); | |
| } | |
| toast.textContent = message; | |
| toast.style.opacity = '1'; | |
| if (toastTimer) { | |
| clearTimeout(toastTimer); | |
| } | |
| toastTimer = setTimeout(() => { | |
| if (toast) { | |
| toast.style.opacity = '0'; | |
| } | |
| }, TOAST_HIDE_MS); | |
| } | |
| function getPlayer() { | |
| return document.getElementById('movie_player'); | |
| } | |
| function ensureCaptionsModule(player) { | |
| if (player && typeof player.loadModule === 'function') { | |
| player.loadModule('captions'); | |
| } | |
| } | |
| function getTracklist(player) { | |
| if (!player || typeof player.getOption !== 'function') { | |
| return null; | |
| } | |
| return player.getOption('captions', 'tracklist'); | |
| } | |
| function normalizeTracklist(tracklist) { | |
| if (Array.isArray(tracklist)) { | |
| return { tracks: tracklist }; | |
| } | |
| if (tracklist && Array.isArray(tracklist.tracks)) { | |
| return tracklist; | |
| } | |
| return null; | |
| } | |
| function normalizeLang(lang) { | |
| if (!lang || typeof lang !== 'string') { | |
| return ''; | |
| } | |
| return lang.toLowerCase(); | |
| } | |
| function matchesLang(track, lang) { | |
| if (!track) { | |
| return false; | |
| } | |
| const code = normalizeLang(track.languageCode); | |
| return code === lang || code.startsWith(`${lang}-`); | |
| } | |
| function findTrack(tracks, lang) { | |
| if (!Array.isArray(tracks)) { | |
| return null; | |
| } | |
| return tracks.find((track) => matchesLang(track, lang)) || null; | |
| } | |
| function getCurrentTrack(player, tracklist) { | |
| if (player && typeof player.getOption === 'function') { | |
| const current = player.getOption('captions', 'track'); | |
| if (current && current.languageCode) { | |
| return current; | |
| } | |
| } | |
| if (tracklist && Array.isArray(tracklist.tracks)) { | |
| const index = tracklist.selectedTrackIndex; | |
| if (Number.isInteger(index) && tracklist.tracks[index]) { | |
| return tracklist.tracks[index]; | |
| } | |
| } | |
| return null; | |
| } | |
| function ensureSubtitlesOn(player) { | |
| if (!player) { | |
| return; | |
| } | |
| if (typeof player.isSubtitlesOn === 'function' && typeof player.toggleSubtitlesOn === 'function') { | |
| if (!player.isSubtitlesOn()) { | |
| player.toggleSubtitlesOn(); | |
| } | |
| return; | |
| } | |
| if (typeof player.isSubtitlesOn === 'function' && typeof player.toggleSubtitles === 'function') { | |
| if (!player.isSubtitlesOn()) { | |
| player.toggleSubtitles(); | |
| } | |
| } | |
| } | |
| function setTrack(player, track) { | |
| if (!player || typeof player.setOption !== 'function') { | |
| return false; | |
| } | |
| player.setOption('captions', 'track', track); | |
| return true; | |
| } | |
| function isWatchPage() { | |
| if (location.hostname === 'youtu.be') { | |
| return true; | |
| } | |
| return location.pathname === '/watch'; | |
| } | |
| function toggleSubtitles() { | |
| if (!isWatchPage()) { | |
| return false; | |
| } | |
| const player = getPlayer(); | |
| if (!player) { | |
| showToast('YouTube player not found.'); | |
| return false; | |
| } | |
| ensureCaptionsModule(player); | |
| const tracklist = normalizeTracklist(getTracklist(player)); | |
| const tracks = tracklist ? tracklist.tracks : null; | |
| if (!tracks || tracks.length === 0) { | |
| showToast('No subtitles available on this video.'); | |
| return false; | |
| } | |
| const englishTrack = findTrack(tracks, 'en'); | |
| const koreanTrack = findTrack(tracks, 'ko'); | |
| if (!englishTrack || !koreanTrack) { | |
| showToast('English or Korean subtitles are not available.'); | |
| return false; | |
| } | |
| const currentTrack = getCurrentTrack(player, tracklist); | |
| const currentLang = normalizeLang(currentTrack && currentTrack.languageCode); | |
| const nextTrack = currentLang.startsWith('en') ? koreanTrack : englishTrack; | |
| if (!setTrack(player, nextTrack)) { | |
| showToast('Unable to switch subtitles.'); | |
| return false; | |
| } | |
| ensureSubtitlesOn(player); | |
| const label = matchesLang(nextTrack, 'ko') ? 'Korean' : 'English'; | |
| showToast(`Subtitles: ${label}`); | |
| return true; | |
| } | |
| window.__ytSubtitleToggle = { | |
| toggle: toggleSubtitles, | |
| version: '0.2', | |
| }; | |
| function isToggleKey(event) { | |
| if (!event) { | |
| return false; | |
| } | |
| return typeof event.key === 'string' && event.key.toLowerCase() === TOGGLE_KEY; | |
| } | |
| document.addEventListener( | |
| 'keydown', | |
| (event) => { | |
| if (event.ctrlKey || event.altKey || event.metaKey) { | |
| return; | |
| } | |
| if (isEditableTarget(event.target)) { | |
| return; | |
| } | |
| if (!isToggleKey(event)) { | |
| return; | |
| } | |
| if (event.repeat) { | |
| return; | |
| } | |
| const now = Date.now(); | |
| if (now - lastToggleAt < TOGGLE_DEBOUNCE_MS) { | |
| return; | |
| } | |
| lastToggleAt = now; | |
| if (toggleSubtitles()) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| } | |
| }, | |
| true | |
| ); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment