Skip to content

Instantly share code, notes, and snippets.

@adolenc
Created February 8, 2026 13:40
Show Gist options
  • Select an option

  • Save adolenc/09cc70bc9ed0cb14d656d88dc57dfa07 to your computer and use it in GitHub Desktop.

Select an option

Save adolenc/09cc70bc9ed0cb14d656d88dc57dfa07 to your computer and use it in GitHub Desktop.
Userscript for toggling between KR and EN subtitles on youtube
// ==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