Skip to content

Instantly share code, notes, and snippets.

@smkplus
Last active December 14, 2025 07:55
Show Gist options
  • Select an option

  • Save smkplus/dcb40b8834d45b60614d677108f39556 to your computer and use it in GitHub Desktop.

Select an option

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

@Pit0o
Copy link

Pit0o commented Dec 14, 2025

خیلی خوب بود دمت گرم
فقط یه مشکلی هست که قسمت تایم لاین رو فارسی نکرده
Screenshot 2025-12-14 112222

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