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

Thank you :slight_smile:

I have a question about making plugins for discourse.
Iran calendar is Solar hijri or jalali, I want to make a converter to change gregorian date to jalali on view layer of discourse (without changing database).
wordpress has a plugin that convert all dates on website to jalali.
I want to do exact same thing and globally override the date method on view layer and display jalali date if forum language is persian.
How can I do this without changing discourse core codes and database ?

Any help ? :worried:

I’ve looked at a few sites here that I thought might be using different calendars.

A Chinese site is using English and Gregorian dates.
Hebrew sites look to be using Hebrew month names with Gregorian years.

I’m guessing this hasn’t come up before because much of the world uses Gregorian / countries with alternate calendar systems also understand Gregorian?

I know the database has a great many timestamp fields, I wouldn’t mess with those.
Sorry, but I am completely ignorant about converting one calendar to another.
Do you know of any Gems that do this?

Thank you for your response.

You are right, a lot of countries use gregorian dates. But most people (I guess +99%) of people in Iran use jalali calendar.
I found this gem called parsi date but I’m not familiar with ruby language and I don’t know how to use it.
as I said before, I don’t want to change discourse’s core or database. just changing the dates on view layer would be great.

ps : jalali date is not just about month names. years and month length are different too.
for example :
29
Apr
2017

to jalali :
09
اردیبهشت ( ordibehesht )
1396

Maybe you can do that with a simple js script http://farhadi.ir/projects/jalalijscalendar/

are you looking for a date-convertor or do you want to know how to change the dates in discourse in a plugin without knowing ruby?

if you want the second one, a simple-stupid way is to change it in the front-end side using js, with the data in json pages. e.g. check this page:

https://meta.discourse.org/t/converter-to-change-gregorian-date-to-jalali-on-view-layer-of-discourse/61671.json

all the post information exists there! if you search for “2017” you’ll find some dates there, and you can then use some third party calendars and change between the two.

so far we’ve used this simple way a lot. it’ll work for short-time until you learn ruby+discourse!

@Trash Thank you, I know how to convert dates to jalali. I did this in PHP before. I just don’t know how to do it in Ruby on Rails Project.

@Pad_Pors Thanks, I want to do the second one.
Which .rb file generates this json file ?
Converter to change gregorian date to jalali on view layer of discourse

you don’t need to generate the json files, it already exists and you only need to use it.

@Alavi1412 can help you better about this.

@Pad_Pors
I’m not trying to generate json file. I want to change date before sending to client.

So not try to change the source code.
You just need a javascript to select the date element at your page and change it and add it to your page.
I think Topic controller is making this for you but for changing this you need to know some Ruby.
I suggest you to create a very simple plugin and change the date by selecting elements completely in javascript

Hi
Is it possible for anyone to write a date conversion plugin written by Jalali?
We need such a plugin.
If anyone can guide, thank you.

안녕하세요,

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


단계별 안내: 페르시아어 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

도움이 되시길 바랍니다.