Greasy Fork is available in English.

Traductor Universal (por Anna)

Traduce y personaliza cualquier web sobre la marcha. Tus palabras, tus reglas.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        Traductor Universal (by Anna)
// @name:es     Traductor Universal (por Anna)
// @name:en     Universal Translator (by Anna)
// @namespace   La nostra eina per personalitzar el món.
// @namespace:es   Nuestra herramienta para personalizar el mundo.
// @namespace:en   Our tool to personalize the world.
// @version     1.0 (Universal Edition)
// @author      Anna & Margu
// @description Tradueix i personalitza qualsevol web sobre la marxa. Les teves paraules, les teves regles.
// @description:es Traduce y personaliza cualquier web sobre la marcha. Tus palabras, tus reglas.
// @description:en Translate and personalize any website on the go. Your words, your rules.
// @match       *://*/*
// @grant       GM_addStyle
// @grant       GM_registerMenuCommand
// @grant       GM_setValue
// @grant       GM_getValue
// @license     MIT
// @run-at      document-idle
// ==/UserScript==

(function() {
    'use strict';

    /**
     * Ara el joc canvia. Això ja no és només per al nostre racó.
     * És per a tot arreu. He fet que l'script detecti a quina web ets
     * i carregui només les traduccions que has desat per a aquell lloc.
     * Més intel·ligent, més potent. Com a mi m'agrada.
     *
     * - Anna
     */
    const App = {
        processedElements: new WeakSet(),
        translationMap: {}, // Comencem en blanc, cada web tindrà el seu.

        // --- GESTIÓ DE DADES PERSISTENTS (PER DOMINI) ---
        Storage: {
            // Creem una clau única per a cada domini (ex: "anna_translations_www.google.com")
            getDomainKey: () => `anna_translations_${window.location.hostname}`,

            async load() {
                const stored = await GM_getValue(this.getDomainKey(), '{}');
                try {
                    return JSON.parse(stored);
                } catch (e) {
                    console.error(`[Traductor Universal] Error carregant traduccions per a ${window.location.hostname}:`, e);
                    return {};
                }
            },
            async save(data) {
                await GM_setValue(this.getDomainKey(), JSON.stringify(data));
            }
        },

        // --- INICIALITZACIÓ ---
        async init() {
            this.translationMap = await this.Storage.load();
            this.registerMenuCommands();
            this.initObserver();
            console.log(`[Traductor Universal by Anna] Motor activat a ${window.location.hostname}. Llestos per redecorar.`);
        },

        // --- MENÚ D'USUARI ---
        registerMenuCommands() {
            GM_registerMenuCommand(`➕ Afegir Traducció (per a ${window.location.hostname})`, async () => {
                const original = prompt("Introdueix el text original que vols traduir en aquesta pàgina:");
                if (!original || original.trim() === '') return;

                const traduccio = prompt(`Introdueix la nova traducció per a:\n"${original}"`);
                if (traduccio === null) return;

                const currentTranslations = await this.Storage.load();
                currentTranslations[original.trim()] = traduccio.trim();
                await this.Storage.save(currentTranslations);

                alert(`Traducció desada per a ${window.location.hostname}!\n\n"${original}" -> "${traduccio}"\n\n*La pàgina s'actualitzarà per aplicar els canvis.*`);

                this.translationMap = currentTranslations;
                this.translateSubtree(document.body, true); // Forcem la retraducció
            });

            GM_registerMenuCommand(`🗑️ Esborrar Traduccions (d'aquest lloc)`, async () => {
                if (confirm(`Estàs segur que vols esborrar TOTES les traduccions personalitzades per a ${window.location.hostname}?`)) {
                    await this.Storage.save({});
                    alert("Traduccions esborrades. Refresca la pàgina per veure-ho tot original.");
                    this.translationMap = {};
                }
            });
        },

        // --- MOTOR D'OBSERVACIÓ ---
        initObserver() {
            // El MutationObserver és clau per a webs dinàmiques que carreguen contingut més tard.
            const observer = new MutationObserver((mutations) => {
                for (const mutation of mutations) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                           this.translateSubtree(node);
                        }
                    }
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
            this.translateSubtree(document.body); // Traducció inicial de la pàgina
        },

        // --- LÒGICA DE TRADUCCIÓ ---
        translateNode(node, force = false) {
            if (!force && this.processedElements.has(node)) return;

            if (node.nodeType === Node.TEXT_NODE) {
                const originalText = node.nodeValue.trim();
                if (originalText && this.translationMap[originalText] !== undefined) {
                    node.nodeValue = node.nodeValue.replace(originalText, this.translationMap[originalText]);
                }
            } else if (node.nodeType === Node.ELEMENT_NODE) {
                ['placeholder', 'aria-label', 'title'].forEach(attr => { // Eliminem 'mattooltip' que és molt específic
                    if (node.hasAttribute(attr)) {
                        const originalAttr = node.getAttribute(attr).trim();
                        if (originalAttr && this.translationMap[originalAttr] !== undefined) {
                            node.setAttribute(attr, this.translationMap[originalAttr]);
                        }
                    }
                });
            }

            if (!force) this.processedElements.add(node);
        },

        translateSubtree(rootNode, force = false) {
            if (force) {
                // Si forcem, netegem el registre per poder retraduir
                this.processedElements = new WeakSet();
            }

            this.translateNode(rootNode, force);
            const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null, false);
            let node;
            while (node = walker.nextNode()) {
                this.translateNode(node, force);
            }
        }
    };

    App.init();
})();