Skip to content

Instantly share code, notes, and snippets.

@smkplus
Last active October 11, 2025 06:46
Show Gist options
  • Save smkplus/dcb40b8834d45b60614d677108f39556 to your computer and use it in GitHub Desktop.
Save smkplus/dcb40b8834d45b60614d677108f39556 to your computer and use it in GitHub Desktop.
Jalali Trello Use InjectCode to use this
// ==UserScript==
// @name          New Jalali Date for Trello (Intl API) - Observer Edition
// @namespace     Seyed Morteza Kamali
// @description   Jalali date updated everywhere on Trello using native Intl.DateTimeFormat, MutationObserver, and universal text scanning.
// @include       https://trello.com/*
// @include       http://trello.com/*
// @version       0.0.10 // Incremented version to reflect fix
// @grant         none
// @require       https://unpkg.com/jalali-moment/dist/jalali-moment.browser.js
// ==/UserScript==
// You still need moment.js for reliable parsing of the non-standard English date strings from Trello.
// We will NOT use its jalali features, just its robust Gregorian parsing.
fetch('https://unpkg.com/jalali-moment/dist/jalali-moment.browser.js')
    .then(response => response.text())
    .then(momentScript => {
        // Execute the moment script
        eval(momentScript);
        const JALALIZED_ATTR = 'data-jalalized-intl';
        const now = moment();
        const currentYear = now.year();
        // Regex for universal text scanning: Matches common Trello dates (e.g., "Oct 11", "Feb 28, 2019, 11:36 AM", "Oct 18 - Oct 26")
        // It's crucial for the universal conversion.
        const DATE_TEXT_REGEX = /(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{1,2}(?:(?:,\s\d{4})|(?:\s(?:at|to)\s\d{1,2}:\d{2}\s(?:AM|PM)?))?(?:\s-\s(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{1,2})?/gi;
        /**
         * Converts a Gregorian Date object to a Jalali date string using the native Intl API.
         * @param {Date} dateObj - The Gregorian Date object.
         * @param {boolean} includeTime - Whether to include the time in the output.
         * @param {boolean} includeYear - Whether to include the year in the output.
         * @returns {string} The formatted Jalali date string (e.g., "۱۴ مهر" or "۱۴ مهر ۱۴۰۴، ۰۹:۰۰").
         */
        function formatToJalali(dateObj, includeTime, includeYear) {
            const options = {
                calendar: 'persian',
                localeMatcher: 'best fit',
                day: 'numeric',
                month: 'long',
                year: includeYear ? 'numeric' : undefined,
                hour: includeTime ? 'numeric' : undefined,
                minute: includeTime ? '2-digit' : undefined,
                hour12: false,
                numberingSystem: 'arab',
            };
            // Use 'fa-IR' locale with 'persian' calendar
            let formattedDate = new Intl.DateTimeFormat('fa-IR', options).format(dateObj);
            // Clean up unwanted characters and ensure proper time formatting/order if time is included
            if (includeTime) {
                const timeOptions = {
                    hour: 'numeric', minute: '2-digit', hour12: false, numberingSystem: 'arab'
                };
                const formattedTime = new Intl.DateTimeFormat('fa-IR', timeOptions).format(dateObj);
                const datePart = new Intl.DateTimeFormat('fa-IR', {
                    calendar: 'persian',
                    day: 'numeric',
                    month: 'long',
                    year: options.year,
                    numberingSystem: 'arab'
                }).format(dateObj);
                // Final format: <date>، <time> (e.g., ۱۴ مهر ۱۴۰۴، ۰۸:۰۰)
                formattedDate = `${datePart}، ${formattedTime}`;
            }
            return formattedDate;
        }
        /**
         * The primary date conversion logic
         */
        function convertDateElement(item, textContent) {
            if (item.getAttribute(JALALIZED_ATTR)) return;
            // If the content already contains Farsi numbers, skip to prevent recursion.
            // This is an extra safety check for text nodes where the first attempt failed.
            if (/[۰-۹]/.test(textContent)) return;
            // --- 1. Date Range Handling (e.g., "Oct 18 - Oct 26")
            const dateRangeMatch = textContent.match(/(\w{3} \d{1,2}) - (\w{3} \d{1,2})/);
            if (dateRangeMatch) {
                const startDateString = dateRangeMatch[1];
                const endDateString = dateRangeMatch[2];
                const startDateMoment = moment(startDateString, 'MMM D');
                const endDateMoment = moment(endDateString, 'MMM D');
                // Fix for missing year (only relevant if Trello omits the year)
                if (startDateMoment.isValid() && startDateMoment.isAfter(now)) {
                    startDateMoment.year(currentYear);
                }
                if (endDateMoment.isValid() && endDateMoment.isAfter(now)) {
                    endDateMoment.year(currentYear);
                }
                if (startDateMoment.isValid() && endDateMoment.isValid()) {
                    const jalaliStartDate = formatToJalali(startDateMoment.toDate(), false, false);
                    const jalaliEndDate = formatToJalali(endDateMoment.toDate(), false, false);
                    const newContent = item.innerHTML.replace(textContent, `${jalaliStartDate} - ${jalaliEndDate}`);
                    if (newContent !== item.innerHTML) {
                        item.innerHTML = newContent;
                        item.setAttribute(JALALIZED_ATTR, 'true');
                    }
                }
                return;
            }
            // --- 2. Single Date/Time Handling
            const formats = [
                'MMM D, YYYY, h:mm A',
                'MMM D, YYYY, H:mm',
                'MMM D, YYYY',
                'MMM D [at] h:mm A',
                'MMM D',
            ];
            let sourceText = textContent;
            let momentDate = moment(sourceText, formats);
            // FIX for relative time ("2 minutes ago"): Try to parse the date from the title attribute
            if (moment.isMoment(momentDate) && !momentDate.isValid() && item.hasAttribute('title')) {
                const titleText = item.getAttribute('title');
                // Check to ensure the title is a date string and not a member name
                if (titleText.match(/\w{3} \d{1,2}, \d{4}/i)) {
                    sourceText = titleText;
                    momentDate = moment(sourceText, formats);
                }
            }
           
            // If the element has been correctly identified as a Moment object, proceed with conversion
            if (moment.isMoment(momentDate) && momentDate.isValid()) {
                const includesYearInText = sourceText.includes(String(momentDate.year())) || sourceText.match(/\d{4}/);
                const includesTimeInText = sourceText.includes(':') || sourceText.includes('AM') || sourceText.includes('PM') || sourceText.includes('at');
               
                // FIXED LOGIC: Adjusts moment.js's tendency to bump ambiguous dates (like "Oct 11") to next year
                if (momentDate.isAfter(now) && !includesYearInText) {
                    // This corrects the next-year-bump. The old -6 was a major bug.
                    momentDate.subtract(1, 'year');
                }
                const shouldIncludeYear = includesYearInText || momentDate.year() !== currentYear;
                const jalaliDate = formatToJalali(momentDate.toDate(), includesTimeInText, shouldIncludeYear);
                item.textContent = jalaliDate;
                // Update title attribute (if it was a date)
                if (item.hasAttribute('title') && item.title.match(/\w{3} \d{1,2}, \d{4}/i)) {
                    const titleMomentDate = moment(item.title, formats);
                    if(titleMomentDate.isValid()) {
                        const jalaliTitleDate = formatToJalali(titleMomentDate.toDate(), true, true);
                        item.setAttribute('title', jalaliTitleDate);
                    }
                }
                item.setAttribute(JALALIZED_ATTR, 'true');
            }
        }
        // --- Universal Text Scanning Function ---
        /**
         * Scans a node's text nodes and replaces Gregorian dates with Jalali dates.
         * This function handles dates embedded in general text (comments, descriptions).
         */
        function scanTextNodesForDates(node) {
            // Use a TreeWalker to efficiently find only the relevant text nodes
            const walker = document.createTreeWalker(
                node,
                NodeFilter.SHOW_TEXT,
                { acceptNode: (n) => {
                    // CRITICAL FIX: Skip text nodes whose parent is already marked as Jalalized
                    // Also skip: form elements, and style/script tags
                    if (n.parentNode.closest(`[${JALALIZED_ATTR}], textarea, input, script, style`)) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    // Filter out text nodes that are just whitespace
                    if (n.nodeValue.trim().length === 0) {
                        return NodeFilter.FILTER_REJECT;
                    }
                   
                    // Also ensure the text node itself doesn't contain Farsi/Arabic numbers (already converted)
                    if (/[۰-۹]/.test(n.nodeValue)) {
                        return NodeFilter.FILTER_REJECT;
                    }
                   
                    return NodeFilter.FILTER_ACCEPT;
                }},
                false
            );
            let currentNode;
            const textNodesToConvert = [];
            while (currentNode = walker.nextNode()) {
                textNodesToConvert.push(currentNode);
            }
           
            textNodesToConvert.forEach(textNode => {
                const text = textNode.nodeValue;
               
                // Find all date matches in the text node's value
                const matches = [...text.matchAll(DATE_TEXT_REGEX)];
               
                if (matches.length > 0) {
                    let lastIndex = 0;
                    const fragment = document.createDocumentFragment();
                    matches.forEach(match => {
                        const dateText = match[0];
                        const matchIndex = match.index;
                        // 1. Append the text *before* the match
                        if (matchIndex > lastIndex) {
                            fragment.appendChild(document.createTextNode(text.substring(lastIndex, matchIndex)));
                        }
                        // 2. Wrap the date in a new span, apply conversion, and append
                        const dateSpan = document.createElement('span');
                        dateSpan.textContent = dateText;
                       
                        try {
                            convertDateElement(dateSpan, dateText);
                            // If successful, dateSpan will have the Jalali date and the JALALIZED_ATTR
                        } catch(e) {
                            // On failure (e.g., regex false positive), revert to original text
                            dateSpan.textContent = dateText;
                        }
                       
                        fragment.appendChild(dateSpan);
                        lastIndex = matchIndex + dateText.length;
                    });
                    // 3. Append the remaining text after the last match
                    if (lastIndex < text.length) {
                        fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
                    }
                   
                    // Replace the original text node with the new fragment of text and spans
                    textNode.parentNode.replaceChild(fragment, textNode);
                }
            });
        }
        // --- Core Execution Logic (Mutation Observer) ---
        // Specific selectors are kept for high-priority Trello elements (due dates, etc.)
        const SPECIFIC_TARGET_SELECTORS = [
            '.js-start-date-badge',
            '.js-due-date-text',
            '.card-back-redesign a[title*="M"]',
            '.js-date-text',
        ];
       
        // A combined selector for the observer to find un-jalalized elements
        const OBSERVER_TARGET_SELECTOR = SPECIFIC_TARGET_SELECTORS.map(s => `${s}:not([${JALALIZED_ATTR}])`).join(', ');
        /**
         * Processes new nodes added to the DOM.
         */
        function processNodes(nodes) {
            nodes.forEach(node => {
                if (node.nodeType === 1) { // Element node
                   
                    // 1. Handle specific Trello elements (due dates, etc.)
                    node.querySelectorAll(OBSERVER_TARGET_SELECTOR).forEach(item => {
                        convertDateElement(item, item.textContent.trim());
                    });
                    // Also check the node itself if it matches a selector
                    if (node.matches(SPECIFIC_TARGET_SELECTORS.join(','))) {
                        convertDateElement(node, node.textContent.trim());
                    }
                    // 2. Scan the element and its children for embedded dates in text nodes (The "everywhere" part)
                    scanTextNodesForDates(node);
                }
            });
        }
       
        // --- Initialize Observer ---
        // The MutationObserver watches for new DOM nodes and calls processNodes on them.
        const observer = new MutationObserver((mutationsList, observer) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    processNodes(mutation.addedNodes);
                }
            }
        });
        // Start observing the document body for all changes in the DOM subtree
        observer.observe(document.body, { childList: true, subtree: true });
        // Initial run to convert dates already on the page when the script loads
        document.querySelectorAll(OBSERVER_TARGET_SELECTOR).forEach(item => {
            convertDateElement(item, item.textContent.trim());
        });
    });
@smkplus
Copy link
Author

smkplus commented Oct 11, 2025

افزونه زیر رو نصب کنید
https://chromewebstore.google.com/detail/inject-code/jpbbdgndcngomphbmplabjginoihkdph?hl=en

کد بالا رو اضافه کنید و ذخیره کنید

image image

بعد از اینکه صفحه رو رفرش کردید تاریخ ها شمسی میشه

image image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment