Universal Translator (by Anna)

Translate and personalize any website on the go. Your words, your rules.

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        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();
})();