GitHub Date Converter

Convert GitHub dates to standard numerical formats, supports custom formats.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name            GitHub Date Converter
// @name:zh         GitHub 日期转换器
// @namespace       github-date-converter
// @version         0.3.0
// @author          dumeng
// @description     Convert GitHub dates to standard numerical formats, supports custom formats.
// @description:zh  将 GitHub 页面中的日期转换为标准的数字格式,支持自定义格式
// @license         MIT
// @icon            https://github.githubassets.com/favicons/favicon.svg
// @homepage        https://github.com/dumeng-chn/github-date-converter
// @homepageURL     https://github.com/dumeng-chn/github-date-converter
// @source          https://github.com/dumeng-chn/github-date-converter.git
// @supportURL      https://github.com/dumeng-chn/github-date-converter/issues
// @match           https://github.com/*
// @require         https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js
// @grant           GM_getValue
// @grant           GM_registerMenuCommand
// @grant           GM_setValue
// @grant           GM_unregisterMenuCommand
// @run-at          document-end
// @compatible      chrome, firefox, edge, safari
// ==/UserScript==

(function (dayjs) {
  'use strict';

  var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_registerMenuCommand = (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
  var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  var _GM_unregisterMenuCommand = (() => typeof GM_unregisterMenuCommand != "undefined" ? GM_unregisterMenuCommand : void 0)();
  const PRESETS = [
    { label: "YYYY-MM-DD (2025-12-31)", format: "YYYY-MM-DD" },
    { label: "MM/DD/YYYY (12/31/2025)", format: "MM/DD/YYYY" },
    { label: "DD/MM/YYYY (31/12/2025)", format: "DD/MM/YYYY" },
    { label: "YYYY/MM/DD (2025/12/31)", format: "YYYY/MM/DD" }
  ];
  const FORMAT_KEY = "dateFormat";
  const RELATIVE_KEY = "relativeMode";
  const IGNORE_FILE_LIST_KEY = "ignoreFileList";
  const LANG_KEY = "language";
  const DEFAULT_FORMAT = "YYYY-MM-DD";
  function getLang() {
    const defaultLang = navigator.language.startsWith("zh") ? "zh" : "en";
    return _GM_getValue(LANG_KEY, defaultLang);
  }
  const I18N = {
    en: {
      customFormatMenu: "✏️ Custom Format",
      customFormatPrompt: "Enter date format (placeholders: YYYY, MM, DD)\nExample: YYYY-MM-DD",
      relativeModeNone: "Do not convert relative time (Default)",
      relativeMode7Days: "Only convert relative time > 7 days",
      relativeModeAll: "Convert all relative time",
      ignoreFileListMenu: "📁 Ignore repository file list dates",
      settingsChangedReload: "Settings changed. Some converted elements require a page reload to restore. Reload now?",
      settingsChangedSimple: "Settings changed. Reload page now?",
      langToggleMenu: "🌐 Language / 语言: English"
    },
    zh: {
      customFormatMenu: "✏️ 自定义格式",
      customFormatPrompt: "请输入日期格式(可用占位符:YYYY 年、MM 月、DD 日)\n示例:YYYY年MM月DD日",
      relativeModeNone: "不转换相对时间(默认)",
      relativeMode7Days: "仅转换大于 7 天的相对时间",
      relativeModeAll: "转换所有相对时间",
      ignoreFileListMenu: "📁 忽略仓库文件列表中的日期",
      settingsChangedReload: "设置已更改。部分已转换的元素需要刷新页面才能还原,是否立即刷新?",
      settingsChangedSimple: "设置已更改。是否立即刷新页面?",
      langToggleMenu: "🌐 Language / 语言: 简体中文"
    }
  };
  function getRelativeMode() {
    const val = _GM_getValue(RELATIVE_KEY, "none");
    return val;
  }
  function formatDateObj(date, format) {
    return dayjs(date).format(format);
  }
  function formatDate(isoDatetime, format) {
    const d = dayjs(isoDatetime);
    if (!d.isValid()) return "";
    return d.format(format);
  }
  const MONTH_PATTERN = /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\b/;
  const TEXT_DATE_PATTERN = /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}(?:,\s+\d{4})?\b/g;
  function processCustomElement(el) {
    const datetime = el.getAttribute("datetime");
    if (!datetime) return;
    const shadow = el.shadowRoot;
    const currentText = (shadow ? shadow.textContent : el.textContent)?.trim() || "";
    const format = _GM_getValue(FORMAT_KEY, DEFAULT_FORMAT);
    const formatted = formatDate(datetime, format);
    if (!formatted) return;
    if (currentText === formatted) return;
    const relativeMode = getRelativeMode();
    const ignoreFileList = _GM_getValue(IGNORE_FILE_LIST_KEY, true);
    if (ignoreFileList && el.closest(".react-directory-commit-age, td.age")) {
      return;
    }
    const isAbsolute = MONTH_PATTERN.test(currentText);
    if (!isAbsolute) {
      if (relativeMode === "none") {
        return;
      } else if (relativeMode === "7days") {
        const ageInDays = Math.abs(Date.now() - new Date(datetime).getTime()) / (1e3 * 60 * 60 * 24);
        if (ageInDays <= 7) return;
      }
    }
    if (shadow) {
      shadow.textContent = formatted;
    } else {
      el.textContent = formatted;
    }
  }
  function processTextElement(el) {
    if (el.children.length > 0) return;
    const originalText = el.getAttribute("data-ghd-original") || el.textContent || "";
    if (!originalText || !originalText.match(TEXT_DATE_PATTERN)) return;
    if (!el.hasAttribute("data-ghd-original")) {
      el.setAttribute("data-ghd-original", originalText);
    }
    const format = _GM_getValue(FORMAT_KEY, DEFAULT_FORMAT);
    const newText = originalText.replace(TEXT_DATE_PATTERN, (match) => {
      let dateStr = match;
      if (!/\d{4}/.test(match)) {
        dateStr = `${match}, ${( new Date()).getFullYear()}`;
      }
      const date = new Date(dateStr);
      if (isNaN(date.getTime())) return match;
      return formatDateObj(date, format);
    });
    if (newText !== el.textContent) {
      el.textContent = newText;
    }
  }
  const TIME_SELECTORS = ["relative-time", "time-ago", "local-time"].join(",");
  const TEXT_SELECTORS = [
    '[data-testid="commit-group-title"]',
".gh-header-meta .Label--secondary",
".release-entry .Label--secondary"
].join(",");
  function processAll() {
    document.querySelectorAll(TIME_SELECTORS).forEach(processCustomElement);
    document.querySelectorAll(TEXT_SELECTORS).forEach(processTextElement);
  }
  function reprocessAll() {
    processAll();
  }
  function startObserver() {
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (mutation.type === "childList") {
          if (mutation.target.nodeType === Node.ELEMENT_NODE) {
            const targetEl = mutation.target;
            if (targetEl.matches(TIME_SELECTORS)) processCustomElement(targetEl);
            if (targetEl.matches(TEXT_SELECTORS)) processTextElement(targetEl);
          }
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType !== Node.ELEMENT_NODE) return;
            const el = node;
            if (el.matches(TIME_SELECTORS)) processCustomElement(el);
            if (el.matches(TEXT_SELECTORS)) processTextElement(el);
            el.querySelectorAll?.(TIME_SELECTORS).forEach(processCustomElement);
            el.querySelectorAll?.(TEXT_SELECTORS).forEach(processTextElement);
          });
        } else if (mutation.type === "attributes" && mutation.attributeName === "title") {
          if (mutation.target.nodeType === Node.ELEMENT_NODE) {
            const targetEl = mutation.target;
            if (targetEl.matches(TIME_SELECTORS)) processCustomElement(targetEl);
          }
        }
      }
    });
    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ["title"]
    });
  }
  let menuIds = [];
  function registerMenuCommands() {
    menuIds.forEach((id) => _GM_unregisterMenuCommand(id));
    menuIds = [];
    const currentFormat = _GM_getValue(FORMAT_KEY, DEFAULT_FORMAT);
    const isCustomFormat = !PRESETS.some((p) => p.format === currentFormat);
    const lang = getLang();
    const t = I18N[lang];
    let separatorCount = 0;
    const addSeparator = () => {
      const spaces = " ".repeat(separatorCount++);
      menuIds.push(_GM_registerMenuCommand("┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈" + spaces, () => {
      }));
    };
    menuIds.push(
      _GM_registerMenuCommand(t.langToggleMenu, () => {
        _GM_setValue(LANG_KEY, lang === "en" ? "zh" : "en");
        registerMenuCommands();
      })
    );
    addSeparator();
    PRESETS.forEach(({ label, format }) => {
      const check = format === currentFormat ? "✅" : "⬛";
      const id = _GM_registerMenuCommand(`${check} 📅 ${label}`, () => {
        _GM_setValue(FORMAT_KEY, format);
        reprocessAll();
        registerMenuCommands();
      });
      menuIds.push(id);
    });
    const customCheck = isCustomFormat ? "✅" : "⬛";
    const customLabel = isCustomFormat ? `${t.customFormatMenu.replace("...", "")} (${currentFormat})` : t.customFormatMenu;
    menuIds.push(
      _GM_registerMenuCommand(`${customCheck} ${customLabel}`, () => {
        const custom = prompt(t.customFormatPrompt, currentFormat);
        if (custom && custom.trim()) {
          _GM_setValue(FORMAT_KEY, custom.trim());
          reprocessAll();
          registerMenuCommands();
        }
      })
    );
    addSeparator();
    const relativeMode = getRelativeMode();
    const relativeOptions = [
      { label: t.relativeModeNone, value: "none" },
      { label: t.relativeMode7Days, value: "7days" },
      { label: t.relativeModeAll, value: "all" }
    ];
    relativeOptions.forEach(({ label, value }) => {
      const check = value === relativeMode ? "✅" : "⬛";
      const id = _GM_registerMenuCommand(`${check} ⏳ ${label}`, () => {
        _GM_setValue(RELATIVE_KEY, value);
        if (confirm(t.settingsChangedReload)) {
          location.reload();
        } else {
          reprocessAll();
          registerMenuCommands();
        }
      });
      menuIds.push(id);
    });
    addSeparator();
    const ignoreFileList = _GM_getValue(IGNORE_FILE_LIST_KEY, true);
    const checkIgnore = ignoreFileList ? "✅" : "⬛";
    menuIds.push(
      _GM_registerMenuCommand(`${checkIgnore} ${t.ignoreFileListMenu}`, () => {
        _GM_setValue(IGNORE_FILE_LIST_KEY, !ignoreFileList);
        if (confirm(t.settingsChangedSimple)) {
          location.reload();
        } else {
          reprocessAll();
          registerMenuCommands();
        }
      })
    );
  }
  processAll();
  startObserver();
  registerMenuCommands();
  document.addEventListener("turbo:load", () => {
    reprocessAll();
  });
  document.addEventListener("pjax:end", () => {
    reprocessAll();
  });
  window.addEventListener("pageshow", (event) => {
    if (event.persisted) {
      reprocessAll();
    }
  });

})(dayjs);