Привет,
Я внес некоторые новые обновления, чтобы учесть ряд условий. Возможно, потребуются дополнительные корректировки.
Пошаговая инструкция: шемси-даты на фронтенде Discourse для персидского языка (если ваш язык использует арабское письмо, внесите соответствующие изменения в код)
1) Создание компонента темы
-
Перейдите в Администрирование → Настройка → Темы
-
Нажмите Компоненты
-
Нажмите Добавить → Создать новый
-
Назовите его: Конвертер дат Шемси (или любое другое название)
2) Добавление скрипта (секция Head)
Внутри компонента:
-
Откройте Общие → Head
-
Вставьте всё, что ниже
-
Сохраните
Код:
<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 уже отправляет обычные григорианские даты в ваш браузер; этот скрипт не изменяет данные, он только заменяет способ отображения текста даты на странице, преобразуя её в шемси (джалали) с использованием самого браузера.
Что делает скрипт (общий обзор)
-
Находит элементы <time>
-
Читает реальную григорианскую дату из атрибута datetime
-
Преобразует её в шемси
-
Заменяет только видимый текст
-
Повторяет это при загрузке новых сообщений
Я также обновил код для решения проблемы с повторной отрисовкой. Это происходит, когда вы переключаетесь с вкладки, и Discourse перерисовывает страницу, возвращая даты к значению по умолчанию. Похоже, проблема решена.
Это даст вам даты следующего вида:
Вы можете посмотреть это онлайн на нашем экземпляре: https://forums.7ho.st
Удачи!