Cigi Spotify Translator

Extract, translate, and display Spotify lyrics with a language selector and manual translation trigger

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Cigi Spotify Translator
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Extract, translate, and display Spotify lyrics with a language selector and manual translation trigger
// @author       Raiwulf
// @match        *://*.spotify.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const DEFAULT_LANGUAGE = 'en';
    let isTranslating = false;

    const languages = {
        // Popular languages
        en: 'English',
        es: 'Spanish',
        fr: 'French',
        de: 'German',
        it: 'Italian',
        pt: 'Portuguese',
        ru: 'Russian',
        ja: 'Japanese',
        ko: 'Korean',
        zh: 'Chinese',
        ar: 'Arabic',
        hi: 'Hindi',
        tr: 'Turkish',
        
        // Rest in alphabetical order
        af: 'Afrikaans',
        sq: 'Albanian',
        am: 'Amharic',
        hy: 'Armenian',
        az: 'Azerbaijani',
        eu: 'Basque',
        be: 'Belarusian',
        bn: 'Bengali',
        bs: 'Bosnian',
        bg: 'Bulgarian',
        ca: 'Catalan',
        ceb: 'Cebuano',
        co: 'Corsican',
        hr: 'Croatian',
        cs: 'Czech',
        da: 'Danish',
        nl: 'Dutch',
        eo: 'Esperanto',
        et: 'Estonian',
        fi: 'Finnish',
        fy: 'Frisian',
        gl: 'Galician',
        ka: 'Georgian',
        el: 'Greek',
        gu: 'Gujarati',
        ht: 'Haitian Creole',
        ha: 'Hausa',
        haw: 'Hawaiian',
        he: 'Hebrew',
        hmn: 'Hmong',
        hu: 'Hungarian',
        is: 'Icelandic',
        ig: 'Igbo',
        id: 'Indonesian',
        ga: 'Irish',
        jv: 'Javanese',
        kn: 'Kannada',
        kk: 'Kazakh',
        km: 'Khmer',
        rw: 'Kinyarwanda',
        ku: 'Kurdish',
        ky: 'Kyrgyz',
        lo: 'Lao',
        la: 'Latin',
        lv: 'Latvian',
        lt: 'Lithuanian',
        lb: 'Luxembourgish',
        mk: 'Macedonian',
        mg: 'Malagasy',
        ms: 'Malay',
        ml: 'Malayalam',
        mt: 'Maltese',
        mi: 'Maori',
        mr: 'Marathi',
        mn: 'Mongolian',
        my: 'Myanmar (Burmese)',
        ne: 'Nepali',
        no: 'Norwegian',
        ny: 'Nyanja (Chichewa)',
        or: 'Odia (Oriya)',
        ps: 'Pashto',
        fa: 'Persian',
        pl: 'Polish',
        pa: 'Punjabi',
        ro: 'Romanian',
        sm: 'Samoan',
        gd: 'Scots Gaelic',
        sr: 'Serbian',
        st: 'Sesotho',
        sn: 'Shona',
        sd: 'Sindhi',
        si: 'Sinhala',
        sk: 'Slovak',
        sl: 'Slovenian',
        so: 'Somali',
        su: 'Sundanese',
        sw: 'Swahili',
        sv: 'Swedish',
        tl: 'Tagalog (Filipino)',
        tg: 'Tajik',
        ta: 'Tamil',
        tt: 'Tatar',
        te: 'Telugu',
        th: 'Thai',
        tk: 'Turkmen',
        uk: 'Ukrainian',
        ur: 'Urdu',
        ug: 'Uyghur',
        uz: 'Uzbek',
        vi: 'Vietnamese',
        cy: 'Welsh',
        xh: 'Xhosa',
        yi: 'Yiddish',
        yo: 'Yoruba',
        zu: 'Zulu'
    };

    function getSavedLanguage() {
        return localStorage.getItem('spotifyLyricsTranslationLang') || DEFAULT_LANGUAGE;
    }

    function saveLanguage(lang) {
        localStorage.setItem('spotifyLyricsTranslationLang', lang);
    }

    async function translateText(text, targetLang) {
        const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
        try {
            const response = await fetch(url);
            const data = await response.json();
            return data[0][0][0];
        } catch (error) {
            console.error('Translation failed:', error);
            return '[Translation Error]';
        }
    }

    async function translateLyrics() {
        if (isTranslating) return;
        isTranslating = true;

        const targetLang = getSavedLanguage();
        const lyricsDivs = document.querySelectorAll('[data-testid="fullscreen-lyric"] div');

        document.querySelectorAll('[data-translated="true"]').forEach(el => el.remove());

        const originalLines = [];
        lyricsDivs.forEach((div, index) => {
            const originalText = div.textContent.trim();
            if (originalText && originalText !== "♪") {
                originalLines.push({ index, text: originalText });
            }
        });

        const translatedLines = await Promise.all(originalLines.map(async (line) => {
            const translatedText = await translateText(line.text, targetLang);
            return { index: line.index, translatedText };
        }));

        translatedLines.forEach(({ index, translatedText }) => {
            const targetDiv = lyricsDivs[index];
            const translationDiv = document.createElement('div');
            translationDiv.style.color = 'gray';
            translationDiv.style.fontStyle = 'italic';
            translationDiv.textContent = translatedText;
            translationDiv.setAttribute('data-translated', 'true');
            targetDiv.parentNode.insertBefore(translationDiv, targetDiv.nextSibling);
        });

        isTranslating = false;
    }

    function observeLyrics() {
        const targetNode = document.querySelector('[data-testid="lyrics-container"]') || document.querySelector('[data-testid="fullscreen-lyric"]');
        if (!targetNode) return;

        const observer = new MutationObserver(() => {
            translateLyrics();
        });

        observer.observe(targetNode, {
            childList: true,
            subtree: true,
        });
    }

    function createHeader() {
        const header = document.createElement('div');
        header.style.cssText = `
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 12px 0;
            background: rgba(40, 40, 40, 0.95);
            width: 100%;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
            margin: 0;
        `;

        const controlsContainer = document.createElement('div');
        controlsContainer.style.cssText = `
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 15px;
            max-width: 600px;
            width: 90%;
        `;

        const selectContainer = document.createElement('div');
        selectContainer.style.cssText = `
            position: relative;
            flex: 0 1 200px;
            min-width: 120px;
        `;

        const selectButton = document.createElement('button');
        selectButton.style.cssText = `
            width: 100%;
            padding: 8px;
            border: none;
            border-radius: 4px;
            background: rgba(80, 80, 80, 1);
            color: white;
            font-size: 14px;
            text-align: left;
            cursor: pointer;
            display: flex;
            justify-content: space-between;
            align-items: center;
        `;
        selectButton.textContent = languages[getSavedLanguage()];

        const dropdown = document.createElement('div');
        dropdown.style.cssText = `
            display: none;
            position: absolute;
            top: 100%;
            left: 0;
            right: 0;
            background: rgba(40, 40, 40, 0.98);
            border-radius: 4px;
            margin-top: 4px;
            max-height: 300px;
            overflow-y: auto;
            z-index: 1000;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
        `;

        const searchInput = document.createElement('input');
        searchInput.style.cssText = `
            width: calc(100% - 16px);
            margin: 8px;
            padding: 8px;
            border: none;
            border-radius: 4px;
            background: rgba(255, 255, 255, 0.1);
            color: white;
            font-size: 14px;
        `;
        searchInput.placeholder = 'Search language...';

        const optionsContainer = document.createElement('div');
        optionsContainer.style.cssText = `
            padding: 8px 0;
        `;

        function createLanguageOptions(filter = '') {
            optionsContainer.innerHTML = '';
            
            // Separate popular and other languages
            const popularLanguages = [
                'en', 'tr', 'pl', 'es', 'fr', 'de', 'pt', 
                'ja', 'it', 'nl'
            ];
            
            const entries = Object.entries(languages);
            const filteredEntries = entries.filter(([_, name]) => 
                name.toLowerCase().includes(filter.toLowerCase())
            );

            // Separate and sort entries
            const popularEntries = filteredEntries.filter(([code]) => 
                popularLanguages.includes(code)
            ).sort((a, b) => 
                popularLanguages.indexOf(a[0]) - popularLanguages.indexOf(b[0])
            );
            
            const otherEntries = filteredEntries.filter(([code]) => 
                !popularLanguages.includes(code)
            );

            // Create divider if both sections have items
            if (popularEntries.length > 0 && otherEntries.length > 0) {
                const divider = document.createElement('div');
                divider.style.cssText = `
                    padding: 8px 16px;
                    color: #888;
                    font-size: 12px;
                    text-transform: uppercase;
                    letter-spacing: 1px;
                `;
                divider.textContent = 'Other Languages';
                
                // Create and append all options
                [...popularEntries, divider, ...otherEntries].forEach(entry => {
                    if (entry instanceof HTMLElement) {
                        optionsContainer.appendChild(entry);
                        return;
                    }

                    const [code, name] = entry;
                    const option = document.createElement('div');
                    option.style.cssText = `
                        padding: 8px 16px;
                        cursor: pointer;
                        color: white;
                        &:hover {
                            background: rgba(255, 255, 255, 0.1);
                        }
                    `;
                    option.textContent = name;
                    option.addEventListener('click', () => {
                        selectButton.textContent = name;
                        dropdown.style.display = 'none';
                        saveLanguage(code);
                        document.querySelectorAll('[data-translated="true"]').forEach(el => el.remove());
                        translateLyrics();
                    });
                    optionsContainer.appendChild(option);
                });
            }
        }

        selectButton.addEventListener('click', () => {
            dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
            if (dropdown.style.display === 'block') {
                searchInput.focus();
            }
        });

        searchInput.addEventListener('input', (e) => {
            createLanguageOptions(e.target.value);
        });

        document.addEventListener('click', (e) => {
            if (!selectContainer.contains(e.target)) {
                dropdown.style.display = 'none';
            }
        });

        createLanguageOptions();
        dropdown.appendChild(searchInput);
        dropdown.appendChild(optionsContainer);
        selectContainer.appendChild(selectButton);
        selectContainer.appendChild(dropdown);

        const translateButton = document.createElement('button');
        translateButton.textContent = 'Translate';
        translateButton.style.cssText = `
            padding: 8px 16px;
            background-color: #1db954;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            min-width: 100px;
        `;

        translateButton.addEventListener('click', () => {
            document.querySelectorAll('[data-translated="true"]').forEach(el => el.remove());
            translateLyrics();
        });

        controlsContainer.appendChild(selectContainer);
        controlsContainer.appendChild(translateButton);
        header.appendChild(controlsContainer);

        const mainView = document.querySelector('.main-view-container__scroll-node-child');
        if (mainView) {
            mainView.insertBefore(header, mainView.firstChild);
        }
    }

    function waitForLyrics() {
        const lyricsContainer = document.querySelector('[data-testid="lyrics-container"]') || document.querySelector('[data-testid="fullscreen-lyric"]');
        if (lyricsContainer) {
            createHeader();
            observeLyrics();
            translateLyrics();
        } else {
            setTimeout(waitForLyrics, 1000);
        }
    }

    function checkForTranslation() {
        setInterval(() => {
            if (!document.querySelector('[data-translated="true"]') && !isTranslating) {
                translateLyrics();
            }
        }, 2000);
    }

    window.addEventListener('load', function () {
        waitForLyrics();
        checkForTranslation();
    });
})();