MyHeritage: Names and dates abbreviator (for Spanish lang)

Intercepts 'get-tree-layout.php' API call and abbreviates the given and last name, along with the months of the dates.

// ==UserScript==
// @name         MyHeritage: Names and dates abbreviator (for Spanish lang)
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  Intercepts 'get-tree-layout.php' API call and abbreviates the given and last name, along with the months of the dates.
// @author       ciricuervo
// @match        https://www.myheritage.es/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=myheritage.com
// @grant        none
// @run-at       document-start
// ==/UserScript==

/**
 *  Considerations:
 *
 * 'get-tree-layout.php' responses are cached in the browser local storage.
 *  Clean the local storage in order to intercept new API calls.
 */

(function() {
    'use strict';

    const monthMap = {
        'enero': 'ene',
        'febrero': 'feb',
        'marzo': 'mar',
        'abril': 'abr',
        'mayo': 'may',
        'junio': 'jun',
        'julio': 'jul',
        'agosto': 'ago',
        'septiembre': 'sep',
        'octubre': 'oct',
        'noviembre': 'nov',
        'diciembre': 'dic',
    };

    // Abbreviator tries not to truncate words
    function abbrev(str, maxLen) {
        if (str.length <= maxLen) return str;
        const cutoff = str.lastIndexOf(' ', maxLen);
        if (cutoff === -1) return str.slice(0, maxLen) + '…'; // there are no spaces
        return str.slice(0, cutoff).trim() + '…';
    }

    function abbreviateDateString(str) {
        return str
            .toLowerCase()
            .replace(/(?<!antes|después|alrededor) de /g, ' ')
            .replace(new RegExp('\\b(' + Object.keys(monthMap).join('|') + ')\\b', 'g'), m => monthMap[m]);
    }

    function processCard(card) {
        if (typeof card.b === 'string') card.b = abbreviateDateString(card.b);
        if (typeof card.d === 'string') card.d = abbreviateDateString(card.d);

        const maxLength = 40;
        if (typeof card.n === 'string' && card.n.length > maxLength && typeof card.fn === 'string' && typeof card.ln === 'string') {

            const prefixLength = Math.max(card.n.indexOf(card.fn), 0);
            const cutoff = Math.floor((maxLength - prefixLength) / 2);

            // First check the last name (we give some extra space for it (+4))
            if (card.ln.length < cutoff + 4) {
                const abbrevLength = Math.max(maxLength - prefixLength - card.ln.length, 1);
                const fnAbbrev = abbrev(card.fn, abbrevLength);
                card.n = card.n.replace(card.fn, fnAbbrev);

            // Then the given name
            } else if (card.fn.length < cutoff) {
                const abbrevLength = Math.max(maxLength - prefixLength - card.fn.length, 1);
                const lnAbbrev = abbrev(card.ln, abbrevLength);
                card.n = card.n.replace(card.ln, lnAbbrev);

            // Else, abbreviate them equally
            } else {
                const fnAbbrev = abbrev(card.fn, cutoff);
                const lnAbbrev = abbrev(card.ln, cutoff);
                card.n = card.n.replace(card.fn, fnAbbrev);
                card.n = card.n.replace(card.ln, lnAbbrev);
            }
        }
    }

    const interceptUrl = 'get-tree-layout.php';

    // Fetch interceptor
    const origFetch = window.fetch;
    window.fetch = async function(input, init) {
        const url = typeof input === 'string' ? input : input.url;
        const resp = await origFetch(input, init);
        if (url.includes(interceptUrl) &&
            resp.headers.get('Content-Type')?.includes('application/json')
           ) {
            const data = await resp.clone().json();
            if (data?.data?.personCards) {
                data.data.personCards.forEach(processCard);
            }
            const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
            return new Response(blob, {
                status: resp.status,
                statusText: resp.statusText,
                headers: resp.headers
            });
        }
        return resp;
    };

    // XHR interceptor
    const origOpen = XMLHttpRequest.prototype.open;
    const origSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function(method, url) {
        this._url = url;
        return origOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function(body) {
        this.addEventListener('readystatechange', function() {
            if (this.readyState === 4 &&
                this._url.includes(interceptUrl) &&
                this.getResponseHeader('Content-Type')?.includes('application/json')
               ) {
                try {
                    const json = JSON.parse(this.responseText);
                    if (json?.data?.personCards) {
                        json.data.personCards.forEach(processCard);
                    }
                    Object.defineProperty(this, 'responseText', {
                        writable: true,
                        value: JSON.stringify(json)
                    });
                } catch (e) {
                    console.error('Error parseando JSON MyHeritage:', e);
                }
            }
        });
        return origSend.apply(this, arguments);
    };

})();