This commit is contained in:
Sacha Pignot 2026-03-18 05:53:04 +00:00 committed by GitHub
commit df449ccb36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1059 additions and 0 deletions

View File

@ -0,0 +1,22 @@
---
title: Recent Changes
tags: component
---
Quartz can generate an activity feed showing recently created and modified notes, with tab-based filtering and pagination. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentChanges` in `quartz.layout.ts`.
## Customization
- Changing the title from "Recent Changes": pass in an additional parameter to `Component.RecentChanges({ title: "Latest updates" })`
- Changing the number of items shown (without filter tabs): `Component.RecentChanges({ limit: 5 })`
- Show only new notes (hide modified): `Component.RecentChanges({ showModified: false })`
- Show only modified notes (hide new): `Component.RecentChanges({ showCreated: false })`
- Enable tab filter UI (All / New / Updated) with Load More pagination: `Component.RecentChanges({ showFilter: true })`
- Set the number of items loaded per tab or per "Load more" click: `Component.RecentChanges({ pageSize: 10 })`
- Show note excerpts (requires `detailed: true`): `Component.RecentChanges({ detailed: true, showExcerpt: true })`
- Show tags (requires `detailed: true`): `Component.RecentChanges({ detailed: true, showTags: true })`
- Restrict to notes under a specific path: `Component.RecentChanges({ filterBy: ["blog/"] })`
- Show a 'see all' link: pass in an additional parameter to `Component.RecentChanges({ linkToMore: "recent-changes" })`. This field should be a full slug to a page that exists.
- Restrict rendering to specific pages: `Component.RecentChanges({ pages: ["index" as FullSlug] })`. Useful when placing the component in a global layout but only wanting it to appear on certain pages.
- Component: `quartz/components/RecentChanges.tsx`
- Style: `quartz/components/styles/recentChanges.scss`

View File

@ -0,0 +1,262 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
import { QuartzPluginData } from "../plugins/vfile"
import { classNames } from "../util/lang"
import { i18n } from "../i18n"
import { GlobalConfiguration } from "../cfg"
import style from "./styles/recentChanges.scss"
import { ChangedItem } from "./utils/recentChanges"
// @ts-ignore
import script from "./scripts/recentChanges.inline"
interface Options {
title?: string
limit: number
showCreated: boolean
showModified: boolean
detailed: boolean
filterBy: string[]
showExcerpt: boolean
showTags: boolean
showFilter: boolean
pageSize: number
linkToMore: SimpleSlug | false
pages: FullSlug[]
}
const defaultOptions = (_cfg: GlobalConfiguration): Options => ({
limit: 10,
showCreated: true,
showModified: true,
detailed: false,
filterBy: [],
showExcerpt: false,
showTags: false,
showFilter: false,
pageSize: 20,
linkToMore: false,
pages: [],
})
function formatRelativeDate(date: Date, locale: string): string {
const diffMs = Date.now() - date.getTime()
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" })
const abs = (n: number) => Math.abs(n)
const s = Math.round(diffMs / 1000)
const m = Math.round(diffMs / 60000)
const h = Math.round(diffMs / 3600000)
const d = Math.round(diffMs / 86400000)
const w = Math.round(diffMs / 604800000)
const mo = Math.round(diffMs / 2592000000)
const y = Math.round(diffMs / 31536000000)
if (abs(s) < 60) return rtf.format(-s, "second")
if (abs(m) < 60) return rtf.format(-m, "minute")
if (abs(h) < 24) return rtf.format(-h, "hour")
if (abs(d) < 7) return rtf.format(-d, "day")
if (abs(w) < 4) return rtf.format(-w, "week")
if (abs(mo) < 12) return rtf.format(-mo, "month")
return rtf.format(-y, "year")
}
export default ((userOpts?: Partial<Options>) => {
const RecentChanges: QuartzComponent = ({
allFiles,
fileData,
displayClass,
cfg,
}: QuartzComponentProps) => {
const opts = { ...defaultOptions(cfg), ...userOpts }
const t = i18n(cfg.locale).components.recentChanges
// Only render on specified pages (empty = all pages)
if (opts.pages.length > 0 && !opts.pages.some((p) => fileData.slug === p)) {
return null
}
// Filter files with valid dates
const validFiles = allFiles.filter(
(file: QuartzPluginData) => file.dates && (file.dates.created || file.dates.modified),
)
// Sort by most recent date
const sortedFiles = validFiles.sort((a: QuartzPluginData, b: QuartzPluginData) => {
const dateA = a.dates?.modified || a.dates?.created || new Date(0)
const dateB = b.dates?.modified || b.dates?.created || new Date(0)
return dateB.getTime() - dateA.getTime()
})
// Convert to ChangedItems
const allItems: ChangedItem[] = sortedFiles.map((file: QuartzPluginData) => {
const created = file.dates?.created
const modified = file.dates?.modified
// A note is "Updated" only if its creation date predates the last modification by >1h.
const isModified =
created !== undefined &&
modified !== undefined &&
modified.getTime() - created.getTime() > 60 * 60 * 1000
return {
title: file.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title,
link: file.slug as FullSlug,
date: file.dates?.modified || file.dates?.created || new Date(),
createdDate: file.dates?.created ?? file.dates?.modified ?? new Date(),
type: isModified ? "modified" : "created",
id: `${file.slug}-${isModified ? "modified" : "created"}`,
excerpt: file.frontmatter?.description,
tags: file.frontmatter?.tags,
}
})
// Apply type filters
let filtered = allItems
if (!opts.showCreated) {
filtered = filtered.filter((item) => item.type !== "created")
}
if (!opts.showModified) {
filtered = filtered.filter((item) => item.type !== "modified")
}
// Apply path filters
if (opts.filterBy.length > 0) {
filtered = filtered.filter((item) => opts.filterBy.some((f) => item.link.includes(f)))
}
const remaining = Math.max(0, filtered.length - opts.limit)
// JSON data island: all items as compact JSON for progressive client-side injection.
// Links are pre-resolved server-side since the client cannot call resolveRelative.
const allItemsJson = JSON.stringify(
filtered.map((item) => ({
title: item.title,
link: resolveRelative(fileData.slug!, item.link),
date: item.date.getTime(),
created: item.createdDate.getTime(),
type: item.type,
...(opts.showExcerpt && item.excerpt ? { excerpt: item.excerpt } : {}),
...(opts.showTags && item.tags?.length ? { tags: item.tags } : {}),
})),
).replace(/<\//g, "<\\/")
// For showFilter: pre-render pageSize items per type so both "New" and "Updated" tabs
// start with visible content. Each item is tagged with its index in filtered (= allData)
// via data-idx so the client can correctly initialize its deduplication set.
// For !showFilter: flat limit cap, no Load More.
const filteredWithIdx = filtered.map((item, idx) => ({ item, idx }))
const initialItems = opts.showFilter
? [
...filteredWithIdx.filter(({ item }) => item.type === "created").slice(0, opts.pageSize),
...filteredWithIdx.filter(({ item }) => item.type === "modified").slice(0, opts.pageSize),
].sort((a, b) => b.item.date.getTime() - a.item.date.getTime())
: filteredWithIdx.slice(0, opts.limit)
// i18n strings passed to the client-side script via data attributes
const i18nData = JSON.stringify({
badgeNew: t.badgeNew,
badgeUpdated: t.badgeUpdated,
noChanges: t.noChanges,
})
return (
<div
class={classNames(displayClass, "recent-changes")}
data-page-size={opts.pageSize}
data-detailed={opts.detailed ? "1" : "0"}
data-show-excerpt={opts.showExcerpt ? "1" : "0"}
data-show-tags={opts.showTags ? "1" : "0"}
data-locale={cfg.locale}
data-i18n={i18nData}
data-load-more-tpl={t.loadMoreTemplate}
>
<h3>
{opts.linkToMore ? (
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>{opts.title ?? t.title}</a>
) : (
(opts.title ?? t.title)
)}
</h3>
{opts.showFilter && (
<div class="recent-changes-filter" role="group" aria-label="Filter changes">
<button data-filter="all" class="active">
{t.filterAll}
</button>
<button data-filter="created">{t.filterNew}</button>
<button data-filter="modified">{t.filterUpdated}</button>
</div>
)}
{opts.showFilter && <p class="rc-tab-desc" aria-live="polite"></p>}
{filtered.length === 0 ? (
<p>{t.noChanges}</p>
) : (
<ul class={`recent-changes-list ${opts.detailed ? "detailed" : "condensed"}`}>
{initialItems.map(({ item, idx }) => (
<li
key={item.id}
class={`recent-change-item ${item.type}`}
data-type={item.type}
data-idx={idx}
>
<a
href={resolveRelative(fileData.slug!, item.link)}
class="recent-change-link internal"
>
<span class="recent-change-title">{item.title}</span>
</a>
<div class="recent-change-meta">
<span class="recent-change-type">
{item.type === "created" ? t.badgeNew : t.badgeUpdated}
</span>
<span class="recent-change-date" data-timestamp={item.date.getTime().toString()}>
{formatRelativeDate(item.date, cfg.locale)}
</span>
</div>
{opts.detailed && opts.showExcerpt && item.excerpt && (
<p class="recent-change-excerpt">{item.excerpt}</p>
)}
{opts.detailed && opts.showTags && item.tags && item.tags.length > 0 && (
<div class="recent-change-tags">
{item.tags.map((tag) => (
<span key={tag} class="recent-change-tag">
{tag}
</span>
))}
</div>
)}
</li>
))}
</ul>
)}
{opts.showFilter && (
// @ts-ignore — dangerouslySetInnerHTML on <script> is valid in Preact
<script
type="application/json"
class="rc-items-data"
dangerouslySetInnerHTML={{ __html: allItemsJson }}
/>
)}
{opts.showFilter && filtered.length > 0 && (
<button class="recent-changes-load-more" style="display:none">
Load more
</button>
)}
{!opts.showFilter && opts.linkToMore && remaining > 0 && (
<div class="recent-changes-more">
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>View all changes </a>
</div>
)}
</div>
)
}
RecentChanges.css = style
RecentChanges.afterDOMLoaded = script
return RecentChanges
}) satisfies QuartzComponentConstructor

View File

@ -23,6 +23,7 @@ import Breadcrumbs from "./Breadcrumbs"
import Comments from "./Comments"
import Flex from "./Flex"
import ConditionalRender from "./ConditionalRender"
import RecentChanges from "./RecentChanges"
export {
ArticleTitle,
@ -50,4 +51,5 @@ export {
Comments,
Flex,
ConditionalRender,
RecentChanges,
}

View File

@ -0,0 +1,237 @@
function formatRelativeDate(date: Date, locale: string): string {
const diffMs = Date.now() - date.getTime()
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" })
const abs = (n: number) => Math.abs(n)
const s = Math.round(diffMs / 1000)
const m = Math.round(diffMs / 60000)
const h = Math.round(diffMs / 3600000)
const d = Math.round(diffMs / 86400000)
const w = Math.round(diffMs / 604800000)
const mo = Math.round(diffMs / 2592000000)
const y = Math.round(diffMs / 31536000000)
if (abs(s) < 60) return rtf.format(-s, "second")
if (abs(m) < 60) return rtf.format(-m, "minute")
if (abs(h) < 24) return rtf.format(-h, "hour")
if (abs(d) < 7) return rtf.format(-d, "day")
if (abs(w) < 4) return rtf.format(-w, "week")
if (abs(mo) < 12) return rtf.format(-mo, "month")
return rtf.format(-y, "year")
}
interface RcItemJson {
title: string
link: string
date: number // most-recent-activity timestamp (ms)
created: number // creation date timestamp (ms)
type: "created" | "modified"
excerpt?: string
tags?: string[]
}
interface RcI18n {
badgeNew: string
badgeUpdated: string
noChanges: string
}
function setupRecentChanges() {
// Read page locale from <html lang="..."> (set by Quartz from cfg.locale)
const pageLocale = document.documentElement.lang || "en-US"
// Refresh relative dates on all pre-rendered items
const dateEls = document.querySelectorAll<HTMLElement>(".recent-change-date[data-timestamp]")
dateEls.forEach((el) => {
const ts = parseInt(el.dataset.timestamp!, 10)
if (!isNaN(ts)) {
el.textContent = formatRelativeDate(new Date(ts), pageLocale)
}
})
const containers = document.querySelectorAll<HTMLElement>(".recent-changes")
containers.forEach((container) => {
const filterGroup = container.querySelector<HTMLElement>(".recent-changes-filter")
if (!filterGroup) return
const locale = container.dataset.locale ?? pageLocale
const pageSize = parseInt(container.dataset.pageSize ?? "20", 10)
const isDetailed = container.dataset.detailed === "1"
const showExcerpt = container.dataset.showExcerpt === "1"
const showTags = container.dataset.showTags === "1"
const loadMoreTpl = container.dataset.loadMoreTpl ?? "Load {count} more · {remaining} remaining"
const i18nData: RcI18n = JSON.parse(container.dataset.i18n ?? "{}")
const list = container.querySelector<HTMLUListElement>(".recent-changes-list")
const loadMoreBtn = container.querySelector<HTMLButtonElement>(".recent-changes-load-more")
const tabDesc = container.querySelector<HTMLParagraphElement>(".rc-tab-desc")
if (!list) return
const safeList = list
const dataScript = container.querySelector<HTMLScriptElement>(".rc-items-data")
if (!dataScript) return
let allData: RcItemJson[]
try {
allData = JSON.parse(dataScript.textContent ?? "[]")
} catch {
return
}
// Three sort views:
// "all" → all notes by most recent activity
// "created" → ALL notes by creation date (the "New" tab)
// "modified" → only modified notes by modification date
const sortedArrays: Record<string, RcItemJson[]> = {
all: [...allData].sort((a, b) => b.date - a.date),
created: [...allData].sort((a, b) => b.created - a.created),
modified: allData.filter((x) => x.type === "modified").sort((a, b) => b.date - a.date),
}
// Human-readable descriptions for each tab
const tabDescriptions: Record<string, string> = {
all: `All ${allData.length} notes · most recent activity first`,
created: `All ${allData.length} notes · sorted by when they were added`,
modified: `${sortedArrays.modified.length} revised notes · latest changes first`,
}
// Per-tab injection pointer
const injectedCount: Record<string, number> = { all: 0, created: 0, modified: 0 }
let currentFilter = localStorage.getItem("recent-changes-filter") ?? "all"
function createItemEl(item: RcItemJson, filter: string): HTMLLIElement {
const li = document.createElement("li")
li.className = `recent-change-item ${item.type}`
li.dataset.type = item.type
const a = document.createElement("a")
a.href = item.link
a.className = "recent-change-link internal"
const titleSpan = document.createElement("span")
titleSpan.className = "recent-change-title"
titleSpan.textContent = item.title
a.appendChild(titleSpan)
li.appendChild(a)
const meta = document.createElement("div")
meta.className = "recent-change-meta"
const typeSpan = document.createElement("span")
typeSpan.className = "recent-change-type"
typeSpan.textContent =
item.type === "created" ? (i18nData.badgeNew ?? "New") : (i18nData.badgeUpdated ?? "Edited")
meta.appendChild(typeSpan)
// Use creation timestamp for the "New" tab, activity timestamp otherwise
const ts = filter === "created" ? item.created : item.date
const dateSpan = document.createElement("span")
dateSpan.className = "recent-change-date"
dateSpan.dataset.timestamp = ts.toString()
dateSpan.textContent = formatRelativeDate(new Date(ts), locale)
meta.appendChild(dateSpan)
li.appendChild(meta)
if (isDetailed && showExcerpt && item.excerpt) {
const p = document.createElement("p")
p.className = "recent-change-excerpt"
p.textContent = item.excerpt
li.appendChild(p)
}
if (isDetailed && showTags && item.tags?.length) {
const tagsDiv = document.createElement("div")
tagsDiv.className = "recent-change-tags"
item.tags.forEach((tag) => {
const tagSpan = document.createElement("span")
tagSpan.className = "recent-change-tag"
tagSpan.textContent = tag
tagsDiv.appendChild(tagSpan)
})
li.appendChild(tagsDiv)
}
return li
}
function updateTabDesc(filter: string) {
if (tabDesc) tabDesc.textContent = tabDescriptions[filter] ?? ""
}
function updateLoadMoreBtn(filter: string) {
if (!loadMoreBtn) return
const arr = sortedArrays[filter] ?? []
const loaded = injectedCount[filter]
const remaining = arr.length - loaded
if (remaining <= 0) {
loadMoreBtn.style.display = "none"
} else {
const count = Math.min(pageSize, remaining)
loadMoreBtn.textContent = loadMoreTpl
.replace("{count}", String(count))
.replace("{remaining}", String(remaining))
loadMoreBtn.style.display = "block"
}
}
function renderTab(filter: string) {
safeList.innerHTML = ""
injectedCount[filter] = 0
const arr = sortedArrays[filter] ?? []
const end = Math.min(pageSize, arr.length)
for (let i = 0; i < end; i++) {
safeList.appendChild(createItemEl(arr[i], filter))
}
injectedCount[filter] = end
updateTabDesc(filter)
updateLoadMoreBtn(filter)
}
function loadMore(filter: string) {
const arr = sortedArrays[filter] ?? []
const start = injectedCount[filter]
const end = Math.min(start + pageSize, arr.length)
for (let i = start; i < end; i++) {
safeList.appendChild(createItemEl(arr[i], filter))
}
injectedCount[filter] = end
updateLoadMoreBtn(filter)
}
const buttons = filterGroup.querySelectorAll<HTMLButtonElement>("button[data-filter]")
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
currentFilter = btn.dataset.filter ?? "all"
localStorage.setItem("recent-changes-filter", currentFilter)
buttons.forEach((b) => b.classList.toggle("active", b === btn))
renderTab(currentFilter)
})
})
if (loadMoreBtn) {
loadMoreBtn.addEventListener("click", () => loadMore(currentFilter))
}
// Restore saved filter button active state
if (currentFilter !== "all") {
const savedBtn = filterGroup.querySelector<HTMLButtonElement>(
`button[data-filter="${currentFilter}"]`,
)
if (savedBtn) {
buttons.forEach((b) => b.classList.remove("active"))
savedBtn.classList.add("active")
}
}
// Initialize: keep SSR items if on "all" tab, otherwise rebuild
if (currentFilter === "all") {
injectedCount.all = safeList.querySelectorAll(".recent-change-item").length
updateTabDesc("all")
updateLoadMoreBtn("all")
} else {
renderTab(currentFilter)
}
})
}
document.addEventListener("nav", setupRecentChanges)

View File

@ -0,0 +1,214 @@
.recent-changes {
margin: 1.5rem 0;
// Badge colors (customizable via CSS custom properties)
--rc-created-bg: #b3e6cc;
--rc-created-text: #005500;
--rc-modified-bg: #d0e0f0;
--rc-modified-text: #003366;
h3 {
font-size: 1.3rem;
margin-bottom: 0.8rem;
color: var(--secondary);
}
.recent-changes-filter {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
button {
padding: 0.35rem 0.75rem;
border: 1px solid var(--lightgray);
border-radius: 4px;
background: transparent;
color: var(--darkgray);
font-size: 0.8rem;
font-family: inherit;
cursor: pointer;
transition:
background-color 0.15s,
color 0.15s;
&.active {
background: var(--secondary);
color: var(--light);
border-color: var(--secondary);
}
&:hover:not(.active) {
background: var(--lightgray);
}
}
}
.rc-tab-desc {
font-size: 0.78rem;
color: var(--gray);
margin: -0.5rem 0 0.75rem;
min-height: 1.1em;
}
.rc-hidden-filter,
.rc-hidden-page {
display: none !important;
}
.recent-changes-load-more {
display: block;
margin: 1rem auto;
padding: 0.5rem 1.5rem;
border: 1px solid var(--lightgray);
border-radius: 4px;
background: transparent;
color: var(--secondary);
font-family: inherit;
font-size: 0.85rem;
cursor: pointer;
transition: background-color 0.15s;
&:hover {
background: var(--lightgray);
}
}
.recent-changes-list {
list-style-type: none;
padding: 0;
margin: 0;
&.detailed {
.recent-change-item {
margin-bottom: 1.5rem;
padding-bottom: 1.2rem;
border-bottom: 1px solid var(--lightgray);
&:last-child {
border-bottom: none;
}
}
.recent-change-excerpt {
margin: 0.5rem 0;
font-size: 0.9rem;
color: var(--gray);
line-height: 1.4;
}
.recent-change-tags {
margin-top: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
.recent-change-tag {
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
background-color: var(--lightgray);
border-radius: 4px;
color: var(--darkgray);
}
}
}
&.condensed {
.recent-change-item {
margin-bottom: 0.5rem;
display: flex;
flex-direction: column;
@media (min-width: 768px) {
flex-direction: row;
align-items: baseline;
justify-content: space-between;
}
}
}
}
.recent-change-item {
position: relative;
&.created {
.recent-change-type {
background-color: var(--rc-created-bg);
color: var(--rc-created-text);
}
}
&.modified {
.recent-change-type {
background-color: var(--rc-modified-bg);
color: var(--rc-modified-text);
}
}
}
.recent-change-link {
font-weight: 500;
text-decoration: none;
color: var(--dark);
&:hover {
text-decoration: underline;
color: var(--secondary);
}
}
.recent-change-meta {
display: flex;
gap: 0.5rem;
align-items: center;
margin-top: 0.25rem;
font-size: 0.8rem;
@media (min-width: 768px) {
.condensed & {
margin-top: 0;
}
}
}
.recent-change-type {
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
}
.recent-change-date {
color: var(--gray);
}
.recent-changes-more {
margin-top: 1rem;
text-align: right;
a {
font-size: 0.85rem;
text-decoration: none;
color: var(--secondary);
&:hover {
text-decoration: underline;
}
}
}
}
// Dark mode adjustments
@media (prefers-color-scheme: dark) {
.recent-changes {
--rc-created-bg: rgba(179, 230, 204, 0.2);
--rc-created-text: #b3e6cc;
--rc-modified-bg: rgba(208, 224, 240, 0.2);
--rc-modified-text: #d0e0f0;
.recent-changes-list.detailed .recent-change-tags .recent-change-tag {
background-color: var(--darkgray);
color: var(--light);
}
}
}

View File

@ -0,0 +1,12 @@
import { FullSlug } from "../../util/path"
export interface ChangedItem {
title: string
link: FullSlug
date: Date // most recent activity (used for "All" and "Updated" sort)
createdDate: Date // first known date (used for "New" sort)
type: "created" | "modified" // display badge only — not a filter
excerpt?: string
tags?: string[]
id: string
}

View File

@ -46,6 +46,16 @@ export default {
title: "آخر الملاحظات",
seeRemainingMore: ({ remaining }) => `تصفح ${remaining} أكثر →`,
},
recentChanges: {
title: "التغييرات الأخيرة",
filterAll: "الكل",
filterNew: "جديد",
filterUpdated: "محدَّث",
loadMoreTemplate: "تحميل {count} أكثر · {remaining} متبقٍ",
noChanges: "لا توجد تغييرات حديثة.",
badgeNew: "جديد",
badgeUpdated: "تم التعديل",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `مقتبس من ${targetSlug}`,
linkToOriginal: "وصلة للملاحظة الرئيسة",

View File

@ -45,6 +45,16 @@ export default {
title: "Notes Recents",
seeRemainingMore: ({ remaining }) => `Vegi ${remaining} més →`,
},
recentChanges: {
title: "Canvis recents",
filterAll: "Tots",
filterNew: "Nous",
filterUpdated: "Actualitzats",
loadMoreTemplate: "Carrega {count} més · {remaining} restants",
noChanges: "No s'han trobat canvis recents.",
badgeNew: "Nou",
badgeUpdated: "Editat",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transcluit de ${targetSlug}`,
linkToOriginal: "Enllaç a l'original",

View File

@ -45,6 +45,16 @@ export default {
title: "Nejnovější poznámky",
seeRemainingMore: ({ remaining }) => `Zobraz ${remaining} dalších →`,
},
recentChanges: {
title: "Nedávné změny",
filterAll: "Vše",
filterNew: "Nové",
filterUpdated: "Aktualizované",
loadMoreTemplate: "Načíst {count} dalších · zbývá {remaining}",
noChanges: "Nebyly nalezeny žádné nedávné změny.",
badgeNew: "Nové",
badgeUpdated: "Upraveno",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Zobrazení ${targetSlug}`,
linkToOriginal: "Odkaz na původní dokument",

View File

@ -45,6 +45,16 @@ export default {
title: "Zuletzt bearbeitete Seiten",
seeRemainingMore: ({ remaining }) => `${remaining} weitere ansehen →`,
},
recentChanges: {
title: "Letzte Änderungen",
filterAll: "Alle",
filterNew: "Neu",
filterUpdated: "Aktualisiert",
loadMoreTemplate: "{count} weitere laden · {remaining} verbleibend",
noChanges: "Keine aktuellen Änderungen gefunden.",
badgeNew: "Neu",
badgeUpdated: "Bearbeitet",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transklusion von ${targetSlug}`,
linkToOriginal: "Link zum Original",

View File

@ -48,6 +48,16 @@ export interface Translation {
title: string
seeRemainingMore: (variables: { remaining: number }) => string
}
recentChanges: {
title: string
filterAll: string
filterNew: string
filterUpdated: string
loadMoreTemplate: string
noChanges: string
badgeNew: string
badgeUpdated: string
}
transcludes: {
transcludeOf: (variables: { targetSlug: FullSlug }) => string
linkToOriginal: string

View File

@ -45,6 +45,16 @@ export default {
title: "Recent Notes",
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`,
},
recentChanges: {
title: "Recent Changes",
filterAll: "All",
filterNew: "New",
filterUpdated: "Updated",
loadMoreTemplate: "Load {count} more · {remaining} remaining",
noChanges: "No recent changes found.",
badgeNew: "New",
badgeUpdated: "Edited",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
linkToOriginal: "Link to original",

View File

@ -45,6 +45,16 @@ export default {
title: "Recent Notes",
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`,
},
recentChanges: {
title: "Recent Changes",
filterAll: "All",
filterNew: "New",
filterUpdated: "Updated",
loadMoreTemplate: "Load {count} more · {remaining} remaining",
noChanges: "No recent changes found.",
badgeNew: "New",
badgeUpdated: "Edited",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
linkToOriginal: "Link to original",

View File

@ -45,6 +45,16 @@ export default {
title: "Notas Recientes",
seeRemainingMore: ({ remaining }) => `Vea ${remaining} más →`,
},
recentChanges: {
title: "Cambios recientes",
filterAll: "Todos",
filterNew: "Nuevos",
filterUpdated: "Actualizados",
loadMoreTemplate: "Cargar {count} más · {remaining} restantes",
noChanges: "No se encontraron cambios recientes.",
badgeNew: "Nuevo",
badgeUpdated: "Editado",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transcluido de ${targetSlug}`,
linkToOriginal: "Enlace al original",

View File

@ -46,6 +46,16 @@ export default {
title: "یادداشت‌های اخیر",
seeRemainingMore: ({ remaining }) => `${remaining} یادداشت دیگر →`,
},
recentChanges: {
title: "تغییرات اخیر",
filterAll: "همه",
filterNew: "جدید",
filterUpdated: "به‌روز شده",
loadMoreTemplate: "بارگذاری {count} بیشتر · {remaining} باقی‌مانده",
noChanges: "هیچ تغییر اخیری یافت نشد.",
badgeNew: "جدید",
badgeUpdated: "ویرایش شده",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `از ${targetSlug}`,
linkToOriginal: "پیوند به اصلی",

View File

@ -45,6 +45,16 @@ export default {
title: "Viimeisimmät muistiinpanot",
seeRemainingMore: ({ remaining }) => `Näytä ${remaining} lisää →`,
},
recentChanges: {
title: "Viimeisimmät muutokset",
filterAll: "Kaikki",
filterNew: "Uudet",
filterUpdated: "Päivitetyt",
loadMoreTemplate: "Lataa {count} lisää · {remaining} jäljellä",
noChanges: "Viimeisimpiä muutoksia ei löydy.",
badgeNew: "Uusi",
badgeUpdated: "Muokattu",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Upote kohteesta ${targetSlug}`,
linkToOriginal: "Linkki alkuperäiseen",

View File

@ -45,6 +45,16 @@ export default {
title: "Notes Récentes",
seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`,
},
recentChanges: {
title: "Modifications récentes",
filterAll: "Tout",
filterNew: "Nouveau",
filterUpdated: "Mis à jour",
loadMoreTemplate: "Charger {count} de plus · {remaining} restants",
noChanges: "Aucune modification récente trouvée.",
badgeNew: "Nouveau",
badgeUpdated: "Modifié",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`,
linkToOriginal: "Lien vers l'original",

View File

@ -46,6 +46,16 @@ export default {
title: "הערות אחרונות",
seeRemainingMore: ({ remaining }) => `עיין ב ${remaining} נוספים →`,
},
recentChanges: {
title: "שינויים אחרונים",
filterAll: "הכל",
filterNew: "חדש",
filterUpdated: "עודכן",
loadMoreTemplate: "טען {count} נוספים · נותרו {remaining}",
noChanges: "לא נמצאו שינויים אחרונים.",
badgeNew: "חדש",
badgeUpdated: "נערך",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `מצוטט מ ${targetSlug}`,
linkToOriginal: "קישור למקורי",

View File

@ -45,6 +45,16 @@ export default {
title: "Legutóbbi jegyzetek",
seeRemainingMore: ({ remaining }) => `${remaining} további megtekintése →`,
},
recentChanges: {
title: "Legutóbbi változások",
filterAll: "Összes",
filterNew: "Új",
filterUpdated: "Frissített",
loadMoreTemplate: "{count} további betöltése · {remaining} maradt",
noChanges: "Nem találhatók legutóbbi változások.",
badgeNew: "Új",
badgeUpdated: "Szerkesztett",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `${targetSlug} áthivatkozása`,
linkToOriginal: "Hivatkozás az eredetire",

View File

@ -45,6 +45,16 @@ export default {
title: "Catatan Terbaru",
seeRemainingMore: ({ remaining }) => `Lihat ${remaining} lagi →`,
},
recentChanges: {
title: "Perubahan Terkini",
filterAll: "Semua",
filterNew: "Baru",
filterUpdated: "Diperbarui",
loadMoreTemplate: "Muat {count} lagi · {remaining} tersisa",
noChanges: "Tidak ada perubahan terkini ditemukan.",
badgeNew: "Baru",
badgeUpdated: "Diedit",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transklusi dari ${targetSlug}`,
linkToOriginal: "Tautan ke asli",

View File

@ -46,6 +46,16 @@ export default {
seeRemainingMore: ({ remaining }) =>
remaining === 1 ? "Vedi 1 altra →" : `Vedi altre ${remaining}`,
},
recentChanges: {
title: "Modifiche recenti",
filterAll: "Tutte",
filterNew: "Nuove",
filterUpdated: "Aggiornate",
loadMoreTemplate: "Carica altri {count} · {remaining} rimanenti",
noChanges: "Nessuna modifica recente trovata.",
badgeNew: "Nuovo",
badgeUpdated: "Modificato",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Inclusione di ${targetSlug}`,
linkToOriginal: "Link all'originale",

View File

@ -45,6 +45,16 @@ export default {
title: "最近の記事",
seeRemainingMore: ({ remaining }) => `さらに${remaining}件 →`,
},
recentChanges: {
title: "最近の変更",
filterAll: "すべて",
filterNew: "新規",
filterUpdated: "更新済み",
loadMoreTemplate: "さらに{count}件表示 · 残り{remaining}件",
noChanges: "最近の変更はありません。",
badgeNew: "新規",
badgeUpdated: "編集済み",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `${targetSlug}のまとめ`,
linkToOriginal: "元記事へのリンク",

View File

@ -45,6 +45,16 @@ export default {
title: "Соңғы жазбалар",
seeRemainingMore: ({ remaining }) => `Тағы ${remaining} жазбаны қарау →`,
},
recentChanges: {
title: "Соңғы өзгерістер",
filterAll: "Барлығы",
filterNew: "Жаңа",
filterUpdated: "Жаңартылған",
loadMoreTemplate: "{count} қосымша жүктеу · {remaining} қалды",
noChanges: "Соңғы өзгерістер табылмады.",
badgeNew: "Жаңа",
badgeUpdated: "Өңделген",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `${targetSlug} кірістіру`,
linkToOriginal: "Бастапқыға сілтеме",

View File

@ -45,6 +45,16 @@ export default {
title: "최근 게시글",
seeRemainingMore: ({ remaining }) => `${remaining}건 더보기 →`,
},
recentChanges: {
title: "최근 변경사항",
filterAll: "전체",
filterNew: "새로운",
filterUpdated: "업데이트됨",
loadMoreTemplate: "{count}개 더 보기 · {remaining}개 남음",
noChanges: "최근 변경사항이 없습니다.",
badgeNew: "새로운",
badgeUpdated: "수정됨",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `${targetSlug}의 포함`,
linkToOriginal: "원본 링크",

View File

@ -45,6 +45,16 @@ export default {
title: "Naujausi Užrašai",
seeRemainingMore: ({ remaining }) => `Peržiūrėti dar ${remaining}`,
},
recentChanges: {
title: "Naujausi pakeitimai",
filterAll: "Visi",
filterNew: "Nauji",
filterUpdated: "Atnaujinti",
loadMoreTemplate: "Įkelti {count} daugiau · liko {remaining}",
noChanges: "Naujausių pakeitimų nerasta.",
badgeNew: "Naujas",
badgeUpdated: "Redaguota",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Įterpimas iš ${targetSlug}`,
linkToOriginal: "Nuoroda į originalą",

View File

@ -45,6 +45,16 @@ export default {
title: "Nylige notater",
seeRemainingMore: ({ remaining }) => `Se ${remaining} til →`,
},
recentChanges: {
title: "Siste endringer",
filterAll: "Alle",
filterNew: "Nye",
filterUpdated: "Oppdaterte",
loadMoreTemplate: "Last inn {count} til · {remaining} gjenstår",
noChanges: "Ingen nylige endringer funnet.",
badgeNew: "Ny",
badgeUpdated: "Redigert",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transkludering of ${targetSlug}`,
linkToOriginal: "Lenke til original",

View File

@ -45,6 +45,16 @@ export default {
title: "Recente notities",
seeRemainingMore: ({ remaining }) => `Zie ${remaining} meer →`,
},
recentChanges: {
title: "Recente wijzigingen",
filterAll: "Alle",
filterNew: "Nieuw",
filterUpdated: "Bijgewerkt",
loadMoreTemplate: "Laad {count} meer · {remaining} resterend",
noChanges: "Geen recente wijzigingen gevonden.",
badgeNew: "Nieuw",
badgeUpdated: "Bewerkt",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Invoeging van ${targetSlug}`,
linkToOriginal: "Link naar origineel",

View File

@ -45,6 +45,16 @@ export default {
title: "Najnowsze notatki",
seeRemainingMore: ({ remaining }) => `Zobacz ${remaining} nastepnych →`,
},
recentChanges: {
title: "Ostatnie zmiany",
filterAll: "Wszystkie",
filterNew: "Nowe",
filterUpdated: "Zaktualizowane",
loadMoreTemplate: "Załaduj {count} więcej · pozostało {remaining}",
noChanges: "Nie znaleziono ostatnich zmian.",
badgeNew: "Nowe",
badgeUpdated: "Edytowane",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Osadzone ${targetSlug}`,
linkToOriginal: "Łącze do oryginału",

View File

@ -45,6 +45,16 @@ export default {
title: "Notas recentes",
seeRemainingMore: ({ remaining }) => `Veja mais ${remaining}`,
},
recentChanges: {
title: "Mudanças recentes",
filterAll: "Todas",
filterNew: "Novas",
filterUpdated: "Atualizadas",
loadMoreTemplate: "Carregar {count} mais · {remaining} restantes",
noChanges: "Nenhuma mudança recente encontrada.",
badgeNew: "Novo",
badgeUpdated: "Editado",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Transcrever de ${targetSlug}`,
linkToOriginal: "Link ao original",

View File

@ -45,6 +45,16 @@ export default {
title: "Notițe recente",
seeRemainingMore: ({ remaining }) => `Vezi încă ${remaining}`,
},
recentChanges: {
title: "Modificări recente",
filterAll: "Toate",
filterNew: "Noi",
filterUpdated: "Actualizate",
loadMoreTemplate: "Încarcă {count} mai multe · {remaining} rămase",
noChanges: "Nu s-au găsit modificări recente.",
badgeNew: "Nou",
badgeUpdated: "Editat",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Extras din ${targetSlug}`,
linkToOriginal: "Legătură către original",

View File

@ -46,6 +46,16 @@ export default {
seeRemainingMore: ({ remaining }) =>
`Посмотреть оставш${getForm(remaining, "уюся", "иеся", "иеся")} ${remaining}`,
},
recentChanges: {
title: "Последние изменения",
filterAll: "Все",
filterNew: "Новые",
filterUpdated: "Обновлённые",
loadMoreTemplate: "Загрузить ещё {count} · осталось {remaining}",
noChanges: "Последних изменений не найдено.",
badgeNew: "Новое",
badgeUpdated: "Изменено",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Переход из ${targetSlug}`,
linkToOriginal: "Ссылка на оригинал",

View File

@ -45,6 +45,16 @@ export default {
title: "บันทึกล่าสุด",
seeRemainingMore: ({ remaining }) => `ดูเพิ่มอีก ${remaining} รายการ →`,
},
recentChanges: {
title: "การเปลี่ยนแปลงล่าสุด",
filterAll: "ทั้งหมด",
filterNew: "ใหม่",
filterUpdated: "อัปเดตแล้ว",
loadMoreTemplate: "โหลดเพิ่ม {count} รายการ · เหลือ {remaining} รายการ",
noChanges: "ไม่พบการเปลี่ยนแปลงล่าสุด",
badgeNew: "ใหม่",
badgeUpdated: "แก้ไขแล้ว",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `รวมข้ามเนื้อหาจาก ${targetSlug}`,
linkToOriginal: "ดูหน้าต้นทาง",

View File

@ -45,6 +45,16 @@ export default {
title: "Son Notlar",
seeRemainingMore: ({ remaining }) => `${remaining} tane daha gör →`,
},
recentChanges: {
title: "Son değişiklikler",
filterAll: "Tümü",
filterNew: "Yeni",
filterUpdated: "Güncellendi",
loadMoreTemplate: "{count} daha yükle · {remaining} kaldı",
noChanges: "Son değişiklik bulunamadı.",
badgeNew: "Yeni",
badgeUpdated: "Düzenlendi",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `${targetSlug} sayfasından alıntı`,
linkToOriginal: "Orijinal bağlantı",

View File

@ -45,6 +45,16 @@ export default {
title: "Останні нотатки",
seeRemainingMore: ({ remaining }) => `Переглянути ще ${remaining}`,
},
recentChanges: {
title: "Останні зміни",
filterAll: "Усі",
filterNew: "Нові",
filterUpdated: "Оновлені",
loadMoreTemplate: "Завантажити ще {count} · залишилось {remaining}",
noChanges: "Останніх змін не знайдено.",
badgeNew: "Нове",
badgeUpdated: "Відредаговано",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Видобуто з ${targetSlug}`,
linkToOriginal: "Посилання на оригінал",

View File

@ -45,6 +45,16 @@ export default {
title: "Ghi chú gần đây",
seeRemainingMore: ({ remaining }) => `Xem thêm ${remaining} ghi chú →`,
},
recentChanges: {
title: "Thay đổi gần đây",
filterAll: "Tất cả",
filterNew: "Mới",
filterUpdated: "Đã cập nhật",
loadMoreTemplate: "Tải thêm {count} · còn {remaining}",
noChanges: "Không tìm thấy thay đổi gần đây.",
badgeNew: "Mới",
badgeUpdated: "Đã chỉnh sửa",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `Trích dẫn toàn bộ từ ${targetSlug}`,
linkToOriginal: "Xem trang gốc",

View File

@ -45,6 +45,16 @@ export default {
title: "最近的笔记",
seeRemainingMore: ({ remaining }) => `查看更多${remaining}篇笔记 →`,
},
recentChanges: {
title: "最近更改",
filterAll: "全部",
filterNew: "新增",
filterUpdated: "已更新",
loadMoreTemplate: "加载更多 {count} 条 · 剩余 {remaining} 条",
noChanges: "未找到最近的更改。",
badgeNew: "新增",
badgeUpdated: "已编辑",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `包含${targetSlug}`,
linkToOriginal: "指向原始笔记的链接",

View File

@ -45,6 +45,16 @@ export default {
title: "最近的筆記",
seeRemainingMore: ({ remaining }) => `查看更多 ${remaining} 篇筆記 →`,
},
recentChanges: {
title: "最近更改",
filterAll: "全部",
filterNew: "新增",
filterUpdated: "已更新",
loadMoreTemplate: "載入更多 {count} 筆 · 剩餘 {remaining} 筆",
noChanges: "未找到最近的更改。",
badgeNew: "新增",
badgeUpdated: "已編輯",
},
transcludes: {
transcludeOf: ({ targetSlug }) => `包含 ${targetSlug}`,
linkToOriginal: "指向原始筆記的連結",