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

Привет,

Я внес некоторые новые обновления, чтобы учесть ряд условий. Возможно, потребуются дополнительные корректировки.


Пошаговая инструкция: шемси-даты на фронтенде Discourse для персидского языка (если ваш язык использует арабское письмо, внесите соответствующие изменения в код)

1) Создание компонента темы

  1. Перейдите в Администрирование → Настройка → Темы

  2. Нажмите Компоненты

  3. Нажмите Добавить → Создать новый

  4. Назовите его: Конвертер дат Шемси (или любое другое название)


2) Добавление скрипта (секция Head)

Внутри компонента:

  1. Откройте Общие → Head

  2. Вставьте всё, что ниже

  3. Сохраните

Код:

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

  // Форматирование
  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) {
    // Разрешаем повторную обработку, если Discourse перезаписал текст
    // Мы помечаем последнее примененное значение, чтобы обнаружить перезапись.
    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) Элементы, основанные на реальном времени
    root.querySelectorAll("time, .relative-date").forEach(processTimeElement);

    // 2) Простые метки временной шкалы
    root.querySelectorAll(
      ".timeline-scrollarea-wrapper .timeline-ago, " +
      ".timeline-scrollarea-wrapper .start-date span, " +
      ".timeline-scrollarea-wrapper .timeline-date-wrapper span"
    ).forEach(processTimelineLabel);
  }

  // Инициализация
  run();

  // ВАЖНО: Отслеживаем изменения текста, а не только добавленные узлы
  const obs = new MutationObserver((muts) => {
    for (const m of muts) {
      // Если добавлены узлы, обработайте их
      if (m.addedNodes && m.addedNodes.length) {
        m.addedNodes.forEach(n => { if (n.nodeType === 1) run(n); });
      }
      // Если изменился текст, повторно выполните обработку для родительского элемента
      if (m.type === "characterData" && m.target && m.target.parentElement) {
        run(m.target.parentElement);
      }
    }
  });

  obs.observe(document.documentElement, {
    subtree: true,
    childList: true,
    characterData: true // <-- это ключевое изменение
  });

  // Повторное применение при активации вкладки (Discourse часто перерисовывает страницу в этот момент)
  function reapplySoon() {
    // Два прохода обрабатывают немедленные и последующие перерисовки
    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. Повторяет это при загрузке новых сообщений

Я также обновил код для решения проблемы с повторной отрисовкой. Это происходит, когда вы переключаетесь с вкладки, и Discourse перерисовывает страницу, возвращая даты к значению по умолчанию. Похоже, проблема решена.


Это даст вам даты следующего вида:

Вы можете посмотреть это онлайн на нашем экземпляре: https://forums.7ho.st

Удачи!