Converter to change gregorian date to jalali on view layer of discourse

안녕하세요,

일부 조건을 처리하기 위해 업데이트를 몇 가지 추가했습니다. 다만 추가적인 조정이 필요할 수도 있습니다.


단계별 안내: 페르시아어 Discourse 프론트엔드에서 샴시(Shamsi) 날짜 표시 (아랍어 사용 시 코드를 적절히 수정하세요)

1) 테마 컴포넌트 생성

  1. 관리자 → 사용자 정의 → 테마로 이동합니다.

  2. 컴포넌트를 클릭합니다.

  3. 추가 → 새로 만들기를 클릭합니다.

  4. 이름을 **Shamsi Date Converter (또는 원하는 이름)**으로 지정합니다.


2) 스크립트 추가 (Head 섹션)

컴포넌트 내부에서:

  1. 공통 → Head를 엽니다.

  2. 아래의 모든 내용을 붙여넣습니다.

  3. 저장합니다.

코드:

<script>
(function () {
  if (!document.documentElement.lang.startsWith("fa")) return;

  // Formatters
  const fullFormatter = new Intl.DateTimeFormat("fa-IR-u-ca-persian", { dateStyle: "medium" });
  const monthYearFormatter = new Intl.DateTimeFormat("fa-IR-u-ca-persian", { year: "numeric", month: "long" });
  const dayMonthFormatter = new Intl.DateTimeFormat("fa-IR-u-ca-persian", { day: "numeric", month: "long" });

  const gregorianMonthsFa = new Map([
    ["ژانویه", 0], ["فوریه", 1], ["مارس", 2], ["آوریل", 3], ["مه", 4], ["ژوئن", 5],
    ["ژوئیه", 6], ["ژوئیهٔ", 6], ["اوت", 7], ["اگوست", 7], ["سپتامبر", 8],
    ["اکتبر", 9], ["نوامبر", 10], ["دسامبر", 11],
  ]);

  function toLatinDigits(str) {
    return (str || "")
      .replace(/[۰-۹]/g, d => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d)))
      .replace(/[٠-٩]/g, d => String("٠١٢٣٤٥٦٧٨٩".indexOf(d)));
  }

  function toDate(value) {
    if (!value) return null;
    if (/^\d{10,13}$/.test(value)) {
      const n = Number(value);
      return new Date(value.length === 10 ? n * 1000 : n);
    }
    const d = new Date(value);
    return isNaN(d.getTime()) ? null : d;
  }

  function processTimeElement(el) {
    // Allow re-processing if Discourse overwrote text
    // We mark last applied value so we can detect overwrite.
    const date =
      toDate(el.getAttribute("datetime")) ||
      toDate(el.getAttribute("data-time")) ||
      toDate(el.dataset && el.dataset.time) ||
      toDate(el.getAttribute("title"));

    if (!date) return;

    const formatted = fullFormatter.format(date);
    if (el.textContent !== formatted) {
      el.textContent = formatted;
    }
  }

  function findTimelineYear(rootEl) {
    const container = rootEl.closest(".timeline-scrollarea-wrapper") || document;
    const candidates = container.querySelectorAll(".timeline-date-wrapper, .start-date span, .timeline-ago");
    for (const el of candidates) {
      const text = (el.textContent || "").trim().replace(/\s+/g, " ");
      const m = text.match(/^(\S+)\s+([۰-۹٠-٩0-9]{4})$/);
      if (!m) continue;
      const year = Number(toLatinDigits(m[2]));
      if (Number.isFinite(year) && year >= 1970 && year <= 2100) return year;
    }
    return new Date().getFullYear();
  }

  function parseGregorianMonthYearFa(text) {
    const cleaned = (text || "").trim().replace(/\s+/g, " ");
    const parts = cleaned.split(" ");
    if (parts.length !== 2) return null;
    const monthIndex = gregorianMonthsFa.get(parts[0]);
    const year = Number(toLatinDigits(parts[1]));
    if (monthIndex === undefined || !Number.isFinite(year) || year < 1970 || year > 2100) return null;
    return new Date(Date.UTC(year, monthIndex, 1));
  }

  function parseGregorianDayMonthFa(text, assumedYear) {
    const cleaned = (text || "").trim().replace(/\s+/g, " ");
    const parts = cleaned.split(" ");
    if (parts.length !== 2) return null;
    const day = Number(toLatinDigits(parts[0]));
    const monthIndex = gregorianMonthsFa.get(parts[1]);
    if (monthIndex === undefined || !Number.isFinite(day) || day < 1 || day > 31) return null;
    const d = new Date(Date.UTC(assumedYear, monthIndex, day));
    return isNaN(d.getTime()) ? null : d;
  }

  function processTimelineLabel(el) {
    const text = (el.textContent || "").trim();
    if (!text) return;

    const d1 = parseGregorianMonthYearFa(text);
    if (d1) {
      const formatted = monthYearFormatter.format(d1);
      if (el.textContent !== formatted) el.textContent = formatted;
      return;
    }

    const year = findTimelineYear(el);
    const d2 = parseGregorianDayMonthFa(text, year);
    if (d2) {
      const formatted = dayMonthFormatter.format(d2);
      if (el.textContent !== formatted) el.textContent = formatted;
    }
  }

  function run(root = document) {
    // 1) Real time-based elements
    root.querySelectorAll("time, .relative-date").forEach(processTimeElement);

    // 2) Timeline plain labels
    root.querySelectorAll(
      ".timeline-scrollarea-wrapper .timeline-ago, " +
      ".timeline-scrollarea-wrapper .start-date span, " +
      ".timeline-scrollarea-wrapper .timeline-date-wrapper span"
    ).forEach(processTimelineLabel);
  }

  // Initial
  run();

  // IMPORTANT: Observe text changes too, not just added nodes
  const obs = new MutationObserver((muts) => {
    for (const m of muts) {
      // If nodes are added, process them
      if (m.addedNodes && m.addedNodes.length) {
        m.addedNodes.forEach(n => { if (n.nodeType === 1) run(n); });
      }
      // If text changes, re-run on the parent element
      if (m.type === "characterData" && m.target && m.target.parentElement) {
        run(m.target.parentElement);
      }
    }
  });

  obs.observe(document.documentElement, {
    subtree: true,
    childList: true,
    characterData: true // <-- this is the key change
  });

  // Re-apply when tab becomes active again (Discourse often re-renders then)
  function reapplySoon() {
    // two passes handle immediate + next tick re-renders
    run();
    setTimeout(run, 250);
    setTimeout(run, 1000);
  }

  document.addEventListener("visibilitychange", () => {
    if (!document.hidden) reapplySoon();
  });

  window.addEventListener("focus", reapplySoon);
})();
</script>

대신 제가 내보낸 파일을 업로드하여 테마에 할당할 수도 있습니다.

discourse-shamsi-date.zip (2.4 KB)


작동 방식

Discourse는 이미 브라우저로 표준 그레고리안 날짜를 전송합니다. 이 스크립트는 데이터를 변경하지 않으며, 페이지에서 날짜 텍스트가 표시되는 방식을 변경하여 브라우저 자체를 통해 샴시(잘랄리) 날짜로 변환할 뿐입니다.

스크립트의 기능 (개요)

  1. 해당 <time> 요소를 찾습니다.

  2. datetime에서 실제 그레고리안 날짜를 읽습니다.

  3. 샴시 날짜로 변환합니다.

  4. 가시적인 텍스트만 대체합니다.

  5. 새 게시물이 로드될 때마다 이 과정을 반복합니다.

재렌더링(re-rendering) 문제와 관련하여 코드도 업데이트했습니다. 이는 탭에서 잠시 벗어나면 Discourse가 페이지를 다시 렌더링하여 기본 날짜로 되돌아가는 현상입니다. 이 문제가 수정된 것으로 보입니다.


이렇게 하면 다음과 같은 날짜가 표시됩니다:

저희 인스턴스에서 온라인으로 확인할 수 있습니다: https://forums.7ho.st

도움이 되시길 바랍니다.