Last active
October 11, 2025 06:46
-
-
Save smkplus/dcb40b8834d45b60614d677108f39556 to your computer and use it in GitHub Desktop.
Jalali Trello Use InjectCode to use this
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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()); | |
| }); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
افزونه زیر رو نصب کنید
https://chromewebstore.google.com/detail/inject-code/jpbbdgndcngomphbmplabjginoihkdph?hl=en
کد بالا رو اضافه کنید و ذخیره کنید
بعد از اینکه صفحه رو رفرش کردید تاریخ ها شمسی میشه