안녕하세요,
일부 조건을 처리하기 위해 업데이트를 몇 가지 추가했습니다. 다만 추가적인 조정이 필요할 수도 있습니다.
단계별 안내: 페르시아어 Discourse 프론트엔드에서 샴시(Shamsi) 날짜 표시 (아랍어 사용 시 코드를 적절히 수정하세요)
1) 테마 컴포넌트 생성
-
관리자 → 사용자 정의 → 테마로 이동합니다.
-
컴포넌트를 클릭합니다.
-
추가 → 새로 만들기를 클릭합니다.
-
이름을 **Shamsi Date Converter (또는 원하는 이름)**으로 지정합니다.
2) 스크립트 추가 (Head 섹션)
컴포넌트 내부에서:
-
공통 → Head를 엽니다.
-
아래의 모든 내용을 붙여넣습니다.
-
저장합니다.
코드:
<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는 이미 브라우저로 표준 그레고리안 날짜를 전송합니다. 이 스크립트는 데이터를 변경하지 않으며, 페이지에서 날짜 텍스트가 표시되는 방식을 변경하여 브라우저 자체를 통해 샴시(잘랄리) 날짜로 변환할 뿐입니다.
스크립트의 기능 (개요)
-
해당
<time>요소를 찾습니다. -
datetime에서 실제 그레고리안 날짜를 읽습니다. -
샴시 날짜로 변환합니다.
-
가시적인 텍스트만 대체합니다.
-
새 게시물이 로드될 때마다 이 과정을 반복합니다.
재렌더링(re-rendering) 문제와 관련하여 코드도 업데이트했습니다. 이는 탭에서 잠시 벗어나면 Discourse가 페이지를 다시 렌더링하여 기본 날짜로 되돌아가는 현상입니다. 이 문제가 수정된 것으로 보입니다.
이렇게 하면 다음과 같은 날짜가 표시됩니다:
저희 인스턴스에서 온라인으로 확인할 수 있습니다: https://forums.7ho.st
도움이 되시길 바랍니다.

