From 36e4cc41a9e74faddabfd22878ea13b6c504209c Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Mon, 05 Feb 2024 04:57:10 +0000
Subject: [PATCH] chore(i18n): refactor and cleanup (#805)

---
 quartz/components/Footer.tsx               |    6 
 docs/configuration.md                      |    1 
 quartz/components/PageTitle.tsx            |    3 
 quartz/cfg.ts                              |    8 
 quartz/plugins/transformers/frontmatter.ts |    5 
 quartz/components/Head.tsx                 |    6 
 quartz/i18n/index.ts                       |   11 +
 quartz/util/lang.ts                        |    8 -
 quartz/components/Explorer.tsx             |    6 
 quartz/plugins/emitters/contentIndex.ts    |    5 
 quartz/plugins/emitters/tagPage.tsx        |    8 
 quartz/plugins/transformers/toc.ts         |   13 -
 quartz/components/renderPage.tsx           |   20 ++
 quartz/components/TableOfContents.tsx      |    6 
 quartz/components/Search.tsx               |   10 
 quartz/i18n/locales/definition.ts          |   63 +++++++
 quartz/components/pages/404.tsx            |    4 
 quartz/components/RecentNotes.tsx          |   15 -
 quartz/components/ArticleTitle.tsx         |    1 
 quartz/i18n/locales/en-US.ts               |   65 ++++++++
 docs/features/i18n.md                      |   18 ++
 quartz/components/Backlinks.tsx            |    6 
 quartz/components/Darkmode.tsx             |    6 
 quartz/plugins/emitters/404.tsx            |   10 
 quartz/components/Date.tsx                 |    5 
 quartz/components/Graph.tsx                |    4 
 docs/index.md                              |    2 
 quartz/components/ExplorerNode.tsx         |    2 
 /dev/null                                  |   38 ----
 quartz/i18n/locales/fr-FR.ts               |   65 ++++++++
 quartz/plugins/emitters/folderPage.tsx     |    8 
 quartz/components/pages/TagContent.tsx     |   23 +-
 quartz/plugins/emitters/contentPage.tsx    |    2 
 quartz/components/pages/FolderContent.tsx  |    8 
 docs/advanced/making plugins.md            |    2 
 35 files changed, 326 insertions(+), 137 deletions(-)

diff --git a/docs/advanced/making plugins.md b/docs/advanced/making plugins.md
index 65209a2..565f5bd 100644
--- a/docs/advanced/making plugins.md
+++ b/docs/advanced/making plugins.md
@@ -278,7 +278,7 @@
           allFiles,
         }
 
-        const content = renderPage(slug, componentData, opts, externalResources)
+        const content = renderPage(cfg, slug, componentData, opts, externalResources)
         const fp = await emit({
           content,
           slug: file.data.slug!,
diff --git a/docs/configuration.md b/docs/configuration.md
index 047f6ca..33d5a57 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -27,6 +27,7 @@
   - `null`: don't use analytics;
   - `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or
   - `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics
+- `locale`: used for [[i18n]] and date formatting
 - `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
   - This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`
   - Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it.
diff --git a/docs/features/i18n.md b/docs/features/i18n.md
new file mode 100644
index 0000000..57547dd
--- /dev/null
+++ b/docs/features/i18n.md
@@ -0,0 +1,18 @@
+---
+title: Internationalization
+---
+
+Internationalization allows users to translate text in the Quartz interface into various supported languages without needing to make extensive code changes. This can be changed via the `locale` [[configuration]] field in `quartz.config.ts`.
+
+The locale field generally follows a certain format: `{language}-{REGION}`
+
+- `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes).
+- `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
+
+> [!tip] Interested in contributing?
+> We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v4/quartz/i18n/locales)! To contribute a translation, do the following things:
+>
+> 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file.
+> 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above.
+> 3. Fill in the translations!
+> 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`.
diff --git a/docs/index.md b/docs/index.md
index cbf8719..f25b6e2 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -31,7 +31,7 @@
 
 ## 🔧 Features
 
-- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], and [many more](./features) right out of the box
+- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]] and [many more](./features) right out of the box
 - Hot-reload for both configuration and content
 - Simple JSX layouts and [[creating components|page components]]
 - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
diff --git a/quartz/cfg.ts b/quartz/cfg.ts
index e7ae783..a477db0 100644
--- a/quartz/cfg.ts
+++ b/quartz/cfg.ts
@@ -1,5 +1,6 @@
 import { ValidDateType } from "./components/Date"
 import { QuartzComponent } from "./components/types"
+import { ValidLocale } from "./i18n"
 import { PluginTypes } from "./plugins/types"
 import { Theme } from "./util/theme"
 
@@ -39,9 +40,12 @@
   /**
    * Allow to translate the date in the language of your choice.
    * Also used for UI translation (default: en-US)
-   * Need to be formated following the IETF language tag format (https://en.wikipedia.org/wiki/IETF_language_tag)
+   * Need to be formated following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag
+   * The first part is the language (en) and the second part is the script/region (US)
+   * Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
+   * Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
    */
-  locale?: string
+  locale: ValidLocale
 }
 
 export interface QuartzConfig {
diff --git a/quartz/components/ArticleTitle.tsx b/quartz/components/ArticleTitle.tsx
index 2484c94..7768de6 100644
--- a/quartz/components/ArticleTitle.tsx
+++ b/quartz/components/ArticleTitle.tsx
@@ -9,6 +9,7 @@
     return null
   }
 }
+
 ArticleTitle.css = `
 .article-title {
   margin: 2rem 0 0 0;
diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx
index 458e48b..573c1c3 100644
--- a/quartz/components/Backlinks.tsx
+++ b/quartz/components/Backlinks.tsx
@@ -1,7 +1,7 @@
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 import style from "./styles/backlinks.scss"
 import { resolveRelative, simplifySlug } from "../util/path"
-import { i18n } from "../i18n/i18next"
+import { i18n } from "../i18n"
 import { classNames } from "../util/lang"
 
 function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentProps) {
@@ -9,7 +9,7 @@
   const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
   return (
     <div class={classNames(displayClass, "backlinks")}>
-      <h3>{i18n(cfg.locale, "backlinks.backlinks")}</h3>
+      <h3>{i18n(cfg.locale).components.backlinks.title}</h3>
       <ul class="overflow">
         {backlinkFiles.length > 0 ? (
           backlinkFiles.map((f) => (
@@ -20,7 +20,7 @@
             </li>
           ))
         ) : (
-          <li>{i18n(cfg.locale, "backlinks.noBacklinksFound")}</li>
+          <li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li>
         )}
       </ul>
     </div>
diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx
index 056e684..62d3c23 100644
--- a/quartz/components/Darkmode.tsx
+++ b/quartz/components/Darkmode.tsx
@@ -4,7 +4,7 @@
 import darkmodeScript from "./scripts/darkmode.inline"
 import styles from "./styles/darkmode.scss"
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
-import { i18n } from "../i18n/i18next"
+import { i18n } from "../i18n"
 import { classNames } from "../util/lang"
 
 function Darkmode({ displayClass, cfg }: QuartzComponentProps) {
@@ -23,7 +23,7 @@
           style="enable-background:new 0 0 35 35"
           xmlSpace="preserve"
         >
-          <title>{i18n(cfg.locale, "darkmode.lightMode")}</title>
+          <title>{i18n(cfg.locale).components.themeToggle.darkMode}</title>
           <path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5    S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5    C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6    C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9    c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44    l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5    c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06    L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z     M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2    C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29    c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7    C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5    c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
         </svg>
       </label>
@@ -39,7 +39,7 @@
           style="enable-background:new 0 0 100 100"
           xmlSpace="preserve"
         >
-          <title>{i18n(cfg.locale, "darkmode.lightMode")}</title>
+          <title>{i18n(cfg.locale).components.themeToggle.lightMode}</title>
           <path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571  C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23  c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369  c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65  c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
         </svg>
       </label>
diff --git a/quartz/components/Date.tsx b/quartz/components/Date.tsx
index 6feac17..26b5964 100644
--- a/quartz/components/Date.tsx
+++ b/quartz/components/Date.tsx
@@ -1,9 +1,10 @@
 import { GlobalConfiguration } from "../cfg"
+import { ValidLocale } from "../i18n"
 import { QuartzPluginData } from "../plugins/vfile"
 
 interface Props {
   date: Date
-  locale?: string
+  locale?: ValidLocale
 }
 
 export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
@@ -17,7 +18,7 @@
   return data.dates?.[cfg.defaultDateType]
 }
 
-export function formatDate(d: Date, locale = "en-US"): string {
+export function formatDate(d: Date, locale: ValidLocale = "en-US"): string {
   return d.toLocaleDateString(locale, {
     year: "numeric",
     month: "short",
diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx
index e964c5a..f701734 100644
--- a/quartz/components/Explorer.tsx
+++ b/quartz/components/Explorer.tsx
@@ -6,10 +6,10 @@
 import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
 import { QuartzPluginData } from "../plugins/vfile"
 import { classNames } from "../util/lang"
+import { i18n } from "../i18n"
 
 // Options interface defined in `ExplorerNode` to avoid circular dependency
 const defaultOptions = {
-  title: "Explorer",
   folderClickBehavior: "collapse",
   folderDefaultState: "collapsed",
   useSavedState: true,
@@ -75,7 +75,7 @@
     jsonTree = JSON.stringify(folders)
   }
 
-  function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
+  function Explorer({ cfg, allFiles, displayClass, fileData }: QuartzComponentProps) {
     constructFileTree(allFiles)
     return (
       <div class={classNames(displayClass, "explorer")}>
@@ -87,7 +87,7 @@
           data-savestate={opts.useSavedState}
           data-tree={jsonTree}
         >
-          <h1>{opts.title}</h1>
+          <h1>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h1>
           <svg
             xmlns="http://www.w3.org/2000/svg"
             width="14"
diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx
index 60966b3..2968a03 100644
--- a/quartz/components/ExplorerNode.tsx
+++ b/quartz/components/ExplorerNode.tsx
@@ -12,7 +12,7 @@
 type OrderEntries = "sort" | "filter" | "map"
 
 export interface Options {
-  title: string
+  title?: string
   folderDefaultState: "collapsed" | "open"
   folderClickBehavior: "collapse" | "link"
   useSavedState: boolean
diff --git a/quartz/components/Footer.tsx b/quartz/components/Footer.tsx
index 40faef9..de472f7 100644
--- a/quartz/components/Footer.tsx
+++ b/quartz/components/Footer.tsx
@@ -1,7 +1,7 @@
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 import style from "./styles/footer.scss"
 import { version } from "../../package.json"
-import { i18n } from "../i18n/i18next"
+import { i18n } from "../i18n"
 
 interface Options {
   links: Record<string, string>
@@ -15,8 +15,8 @@
       <footer class={`${displayClass ?? ""}`}>
         <hr />
         <p>
-          {i18n(cfg.locale, "footer.createdWith")}{" "}
-          <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}
+          {i18n(cfg.locale).components.footer.createdWith}{" "}
+          <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year}
         </p>
         <ul>
           {Object.entries(links).map(([text, link]) => (
diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx
index f728c5e..9fce9bd 100644
--- a/quartz/components/Graph.tsx
+++ b/quartz/components/Graph.tsx
@@ -2,7 +2,7 @@
 // @ts-ignore
 import script from "./scripts/graph.inline"
 import style from "./styles/graph.scss"
-import { i18n } from "../i18n/i18next"
+import { i18n } from "../i18n"
 import { classNames } from "../util/lang"
 
 export interface D3Config {
@@ -59,7 +59,7 @@
     const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
     return (
       <div class={classNames(displayClass, "graph")}>
-        <h3>{i18n(cfg.locale, "graph.graphView")}</h3>
+        <h3>{i18n(cfg.locale).components.graph.title}</h3>
         <div class="graph-outer">
           <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
           <svg
diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx
index b49c385..b94909c 100644
--- a/quartz/components/Head.tsx
+++ b/quartz/components/Head.tsx
@@ -1,13 +1,13 @@
-import { i18n } from "../i18n/i18next"
+import { i18n } from "../i18n"
 import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path"
 import { JSResourceToScriptElement } from "../util/resources"
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
 export default (() => {
   function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
-    const title = fileData.frontmatter?.title ?? i18n(cfg.locale, "head.untitled")
+    const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
     const description =
-      fileData.description?.trim() ?? i18n(cfg.locale, "head.noDescriptionProvided")
+      fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description
     const { css, js } = externalResources
 
     const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
diff --git a/quartz/components/PageTitle.tsx b/quartz/components/PageTitle.tsx
index fb1660a..d129602 100644
--- a/quartz/components/PageTitle.tsx
+++ b/quartz/components/PageTitle.tsx
@@ -1,9 +1,10 @@
 import { pathToRoot } from "../util/path"
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 import { classNames } from "../util/lang"
+import { i18n } from "../i18n"
 
 function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) {
-  const title = cfg?.pageTitle ?? "Untitled Quartz"
+  const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
   const baseDir = pathToRoot(fileData.slug!)
   return (
     <h1 class={classNames(displayClass, "page-title")}>
diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx
index 240ef98..f8f6de4 100644
--- a/quartz/components/RecentNotes.tsx
+++ b/quartz/components/RecentNotes.tsx
@@ -5,11 +5,11 @@
 import style from "./styles/recentNotes.scss"
 import { Date, getDate } from "./Date"
 import { GlobalConfiguration } from "../cfg"
-import { i18n } from "../i18n/i18next"
+import { i18n } from "../i18n"
 import { classNames } from "../util/lang"
 
 interface Options {
-  title: string
+  title?: string
   limit: number
   linkToMore: SimpleSlug | false
   filter: (f: QuartzPluginData) => boolean
@@ -17,7 +17,6 @@
 }
 
 const defaultOptions = (cfg: GlobalConfiguration): Options => ({
-  title: "Recent Notes",
   limit: 3,
   linkToMore: false,
   filter: () => true,
@@ -31,10 +30,10 @@
     const remaining = Math.max(0, pages.length - opts.limit)
     return (
       <div class={classNames(displayClass, "recent-notes")}>
-        <h3>{opts.title}</h3>
+        <h3>{opts.title ?? i18n(cfg.locale).components.recentNotes.title}</h3>
         <ul class="recent-ul">
           {pages.slice(0, opts.limit).map((page) => {
-            const title = page.frontmatter?.title
+            const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title
             const tags = page.frontmatter?.tags ?? []
 
             return (
@@ -72,11 +71,7 @@
         {opts.linkToMore && remaining > 0 && (
           <p>
             <a href={resolveRelative(fileData.slug!, opts.linkToMore)}>
-              {" "}
-              {i18n(cfg.locale, "recentNotes.seeRemainingMore", {
-                remaining: remaining.toString(),
-              })}{" "}
-              →
+              {i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })}
             </a>
           </p>
         )}
diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx
index b73ce0b..a07dbc4 100644
--- a/quartz/components/Search.tsx
+++ b/quartz/components/Search.tsx
@@ -3,7 +3,7 @@
 // @ts-ignore
 import script from "./scripts/search.inline"
 import { classNames } from "../util/lang"
-import { i18n } from "../i18n/i18next"
+import { i18n } from "../i18n"
 
 export interface SearchOptions {
   enablePreview: boolean
@@ -16,11 +16,11 @@
 export default ((userOpts?: Partial<SearchOptions>) => {
   function Search({ displayClass, cfg }: QuartzComponentProps) {
     const opts = { ...defaultOptions, ...userOpts }
-
+    const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
     return (
       <div class={classNames(displayClass, "search")}>
         <div id="search-icon">
-          <p>{i18n(cfg.locale, "search")}</p>
+          <p>{i18n(cfg.locale).components.search.title}</p>
           <div></div>
           <svg
             tabIndex={0}
@@ -44,8 +44,8 @@
               id="search-bar"
               name="search"
               type="text"
-              aria-label="Search for something"
-              placeholder="Search for something"
+              aria-label={searchPlaceholder}
+              placeholder={searchPlaceholder}
             />
             <div id="search-layout" data-preview={opts.enablePreview}></div>
           </div>
diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx
index 2e01507..2abc74b 100644
--- a/quartz/components/TableOfContents.tsx
+++ b/quartz/components/TableOfContents.tsx
@@ -5,7 +5,7 @@
 
 // @ts-ignore
 import script from "./scripts/toc.inline"
-import { i18n } from "../i18n/i18next"
+import { i18n } from "../i18n"
 
 interface Options {
   layout: "modern" | "legacy"
@@ -23,7 +23,7 @@
   return (
     <div class={classNames(displayClass, "toc")}>
       <button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
-        <h3>{i18n(cfg.locale, "tableOfContent")}</h3>
+        <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
         <svg
           xmlns="http://www.w3.org/2000/svg"
           width="24"
@@ -63,7 +63,7 @@
   return (
     <details id="toc" open={!fileData.collapseToc}>
       <summary>
-        <h3>{i18n(cfg.locale, "tableOfContent")}</h3>
+        <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
       </summary>
       <ul>
         {fileData.toc.map((tocEntry) => (
diff --git a/quartz/components/pages/404.tsx b/quartz/components/pages/404.tsx
index 56adbf9..276d5e5 100644
--- a/quartz/components/pages/404.tsx
+++ b/quartz/components/pages/404.tsx
@@ -1,11 +1,11 @@
-import { i18n } from "../../i18n/i18next"
+import { i18n } from "../../i18n"
 import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
 
 function NotFound({ cfg }: QuartzComponentProps) {
   return (
     <article class="popover-hint">
       <h1>404</h1>
-      <p>{i18n(cfg.locale, "404")}</p>
+      <p>{i18n(cfg.locale).pages.error.notFound}</p>
     </article>
   )
 }
diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx
index 02938e3..b954ea2 100644
--- a/quartz/components/pages/FolderContent.tsx
+++ b/quartz/components/pages/FolderContent.tsx
@@ -5,9 +5,8 @@
 import { PageList } from "../PageList"
 import { _stripSlashes, simplifySlug } from "../../util/path"
 import { Root } from "hast"
-import { pluralize } from "../../util/lang"
 import { htmlToJsx } from "../../util/jsx"
-import { i18n } from "../../i18n/i18next"
+import { i18n } from "../../i18n"
 
 interface FolderContentOptions {
   /**
@@ -54,8 +53,9 @@
         <div class="page-listing">
           {options.showFolderCount && (
             <p>
-              {pluralize(allPagesInFolder.length, i18n(cfg.locale, "common.item"))}{" "}
-              {i18n(cfg.locale, "folderContent.underThisFolder")}.
+              {i18n(cfg.locale).pages.folderContent.itemsUnderFolder({
+                count: allPagesInFolder.length,
+              })}
             </p>
           )}
           <div>
diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx
index 57a6c32..19452ec 100644
--- a/quartz/components/pages/TagContent.tsx
+++ b/quartz/components/pages/TagContent.tsx
@@ -4,9 +4,8 @@
 import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
 import { QuartzPluginData } from "../../plugins/vfile"
 import { Root } from "hast"
-import { pluralize } from "../../util/lang"
 import { htmlToJsx } from "../../util/jsx"
-import { i18n } from "../../i18n/i18next"
+import { i18n } from "../../i18n"
 
 const numPages = 10
 function TagContent(props: QuartzComponentProps) {
@@ -44,10 +43,7 @@
         <article>
           <p>{content}</p>
         </article>
-        <p>
-          {i18n(cfg.locale, "tagContent.found")} {tags.length}{" "}
-          {i18n(cfg.locale, "tagContent.totalTags")}.
-        </p>
+        <p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
         <div>
           {tags.map((tag) => {
             const pages = tagItemMap.get(tag)!
@@ -68,10 +64,12 @@
                 {content && <p>{content}</p>}
                 <div class="page-listing">
                   <p>
-                    {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "}
-                    {i18n(cfg.locale, "tagContent.withThisTag")}.{" "}
-                    {pages.length > numPages &&
-                      `${i18n(cfg.locale, "tagContent.showingFirst")} ${numPages}.`}
+                    {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
+                    {pages.length > numPages && (
+                      <span>
+                        {i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })}
+                      </span>
+                    )}
                   </p>
                   <PageList limit={numPages} {...listProps} />
                 </div>
@@ -92,10 +90,7 @@
       <div class={classes}>
         <article>{content}</article>
         <div class="page-listing">
-          <p>
-            {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "}
-            {i18n(cfg.locale, "tagContent.withThisTag")}.
-          </p>
+          <p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
           <div>
             <PageList {...listProps} />
           </div>
diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx
index b3fe06b..fa8305d 100644
--- a/quartz/components/renderPage.tsx
+++ b/quartz/components/renderPage.tsx
@@ -7,6 +7,8 @@
 import { visit } from "unist-util-visit"
 import { Root, Element, ElementContent } from "hast"
 import { QuartzPluginData } from "../plugins/vfile"
+import { GlobalConfiguration } from "../cfg"
+import { i18n } from "../i18n"
 
 interface RenderComponents {
   head: QuartzComponent
@@ -63,6 +65,7 @@
 }
 
 export function renderPage(
+  cfg: GlobalConfiguration,
   slug: FullSlug,
   componentData: QuartzComponentProps,
   components: RenderComponents,
@@ -136,7 +139,9 @@
               type: "element",
               tagName: "a",
               properties: { href: inner.properties?.href, class: ["internal"] },
-              children: [{ type: "text", value: `Link to original` }],
+              children: [
+                { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
+              ],
             },
           ]
         } else if (page.htmlAst) {
@@ -147,7 +152,14 @@
               tagName: "h1",
               properties: {},
               children: [
-                { type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
+                {
+                  type: "text",
+                  value:
+                    page.frontmatter?.title ??
+                    i18n(cfg.locale).components.transcludes.transcludeOf({
+                      targetSlug: page.slug!,
+                    }),
+                },
               ],
             },
             ...(page.htmlAst.children as ElementContent[]).map((child) =>
@@ -157,7 +169,9 @@
               type: "element",
               tagName: "a",
               properties: { href: inner.properties?.href, class: ["internal"] },
-              children: [{ type: "text", value: `Link to original` }],
+              children: [
+                { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
+              ],
             },
           ]
         }
diff --git a/quartz/i18n/i18next.ts b/quartz/i18n/i18next.ts
deleted file mode 100644
index 39c4461..0000000
--- a/quartz/i18n/i18next.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import en from "./locales/en.json"
-import fr from "./locales/fr.json"
-
-const TRANSLATION = {
-  "en-US": en,
-  "fr-FR": fr,
-} as const
-
-type TranslationOptions = {
-  [key: string]: string
-}
-
-export const i18n = (lang = "en-US", key: string, options?: TranslationOptions) => {
-  const locale =
-    Object.keys(TRANSLATION).find(
-      (key) =>
-        key.toLowerCase() === lang.toLowerCase() || key.toLowerCase().includes(lang.toLowerCase()),
-    ) ?? "en-US"
-  const getTranslation = (key: string) => {
-    const keys = key.split(".")
-    let translationString: string | Record<string, unknown> =
-      TRANSLATION[locale as keyof typeof TRANSLATION]
-    keys.forEach((key) => {
-      // @ts-ignore
-      translationString = translationString[key]
-    })
-    return translationString
-  }
-  if (options) {
-    let translationString = getTranslation(key).toString()
-    Object.keys(options).forEach((key) => {
-      translationString = translationString.replace(`{{${key}}}`, options[key])
-    })
-    return translationString
-  }
-  return getTranslation(key).toString()
-}
diff --git a/quartz/i18n/index.ts b/quartz/i18n/index.ts
new file mode 100644
index 0000000..47645c5
--- /dev/null
+++ b/quartz/i18n/index.ts
@@ -0,0 +1,11 @@
+import { Translation } from "./locales/definition"
+import en from "./locales/en-US"
+import fr from "./locales/fr-FR"
+
+export const TRANSLATIONS = {
+  "en-US": en,
+  "fr-FR": fr,
+} as const
+
+export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale]
+export type ValidLocale = keyof typeof TRANSLATIONS
diff --git a/quartz/i18n/locales/definition.ts b/quartz/i18n/locales/definition.ts
new file mode 100644
index 0000000..de607eb
--- /dev/null
+++ b/quartz/i18n/locales/definition.ts
@@ -0,0 +1,63 @@
+import { FullSlug } from "../../util/path"
+
+export interface Translation {
+  propertyDefaults: {
+    title: string
+    description: string
+  }
+  components: {
+    backlinks: {
+      title: string
+      noBacklinksFound: string
+    }
+    themeToggle: {
+      lightMode: string
+      darkMode: string
+    }
+    explorer: {
+      title: string
+    }
+    footer: {
+      createdWith: string
+    }
+    graph: {
+      title: string
+    }
+    recentNotes: {
+      title: string
+      seeRemainingMore: (variables: { remaining: number }) => string
+    }
+    transcludes: {
+      transcludeOf: (variables: { targetSlug: FullSlug }) => string
+      linkToOriginal: string
+    }
+    search: {
+      title: string
+      searchBarPlaceholder: string
+    }
+    tableOfContents: {
+      title: string
+    }
+  }
+  pages: {
+    rss: {
+      recentNotes: string
+      lastFewNotes: (variables: { count: number }) => string
+    }
+    error: {
+      title: string
+      notFound: string
+    }
+    folderContent: {
+      folder: string
+      itemsUnderFolder: (variables: { count: number }) => string
+    }
+    tagContent: {
+      tag: string
+      tagIndex: string
+      itemsUnderTag: (variables: { count: number }) => string
+      showingFirst: (variables: { count: number }) => string
+      totalTags: (variables: { count: number }) => string
+    }
+  }
+}
diff --git a/quartz/i18n/locales/en-US.ts b/quartz/i18n/locales/en-US.ts
new file mode 100644
index 0000000..6ba6d0e
--- /dev/null
+++ b/quartz/i18n/locales/en-US.ts
@@ -0,0 +1,65 @@
+import { Translation } from "./definition"
+
+export default {
+  propertyDefaults: {
+    title: "Untitled",
+    description: "No description provided",
+  },
+  components: {
+    backlinks: {
+      title: "Backlinks",
+      noBacklinksFound: "No backlinks found",
+    },
+    themeToggle: {
+      lightMode: "Light mode",
+      darkMode: "Dark mode",
+    },
+    explorer: {
+      title: "Explorer",
+    },
+    footer: {
+      createdWith: "Created with",
+    },
+    graph: {
+      title: "Graph View",
+    },
+    recentNotes: {
+      title: "Recent Notes",
+      seeRemainingMore: ({ remaining }) => `See ${remaining} more →`,
+    },
+    transcludes: {
+      transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
+      linkToOriginal: "Link to original",
+    },
+    search: {
+      title: "Search",
+      searchBarPlaceholder: "Search for something",
+    },
+    tableOfContents: {
+      title: "Table of Contents",
+    },
+  },
+  pages: {
+    rss: {
+      recentNotes: "Recent notes",
+      lastFewNotes: ({ count }) => `Last ${count} notes`,
+    },
+    error: {
+      title: "Not Found",
+      notFound: "Either this page is private or doesn't exist.",
+    },
+    folderContent: {
+      folder: "Folder",
+      itemsUnderFolder: ({ count }) =>
+        count === 1 ? "1 item under this folder" : `${count} items under this folder.`,
+    },
+    tagContent: {
+      tag: "Tag",
+      tagIndex: "Tag Index",
+      itemsUnderTag: ({ count }) =>
+        count === 1 ? "1 item with this tag" : `${count} items with this tag.`,
+      showingFirst: ({ count }) => `Showing first ${count} tags.`,
+      totalTags: ({ count }) => `Found ${count} total tags.`,
+    },
+  },
+} as const satisfies Translation
diff --git a/quartz/i18n/locales/en.json b/quartz/i18n/locales/en.json
deleted file mode 100644
index fc3cebb..0000000
--- a/quartz/i18n/locales/en.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
-  "404": "Either this page is private or doesn't exist.",
-  "backlinks": {
-    "backlinks": "Backlinks",
-    "noBacklinksFound": "No backlinks found"
-  },
-  "common": {
-    "item": "item"
-  },
-  "darkmode": {
-    "lightMode": "Light mode"
-  },
-  "folderContent": {
-    "underThisFolder": "under this folder"
-  },
-  "footer": {
-    "createdWith": "Created with"
-  },
-  "graph": {
-    "graphView": "Graph View"
-  },
-  "head": {
-    "noDescriptionProvided": "No description provided",
-    "untitled": "Untitled"
-  },
-  "recentNotes": {
-    "seeRemainingMore": "See {{remaining}} more"
-  },
-  "search": "Search",
-  "tableOfContent": "Table of Contents",
-  "tagContent": {
-    "showingFirst": "Showing first",
-    "totalTags": "total tags",
-    "withThisTag": "with this tag",
-    "found": "Found"
-  }
-}
diff --git a/quartz/i18n/locales/fr-FR.ts b/quartz/i18n/locales/fr-FR.ts
new file mode 100644
index 0000000..b30ff04
--- /dev/null
+++ b/quartz/i18n/locales/fr-FR.ts
@@ -0,0 +1,65 @@
+import { Translation } from "./definition"
+
+export default {
+  propertyDefaults: {
+    title: "Sans titre",
+    description: "Aucune description fournie",
+  },
+  components: {
+    backlinks: {
+      title: "Liens retour",
+      noBacklinksFound: "Aucun lien retour trouvé",
+    },
+    themeToggle: {
+      lightMode: "Mode clair",
+      darkMode: "Mode sombre",
+    },
+    explorer: {
+      title: "Explorateur",
+    },
+    footer: {
+      createdWith: "Créé avec",
+    },
+    graph: {
+      title: "Vue Graphique",
+    },
+    recentNotes: {
+      title: "Notes Récentes",
+      seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`,
+    },
+    transcludes: {
+      transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`,
+      linkToOriginal: "Lien vers l'original",
+    },
+    search: {
+      title: "Recherche",
+      searchBarPlaceholder: "Rechercher quelque chose",
+    },
+    tableOfContents: {
+      title: "Table des Matières",
+    },
+  },
+  pages: {
+    rss: {
+      recentNotes: "Notes récentes",
+      lastFewNotes: ({ count }) => `Les dernières ${count} notes`,
+    },
+    error: {
+      title: "Pas trouvé",
+      notFound: "Cette page est soit privée, soit elle n'existe pas.",
+    },
+    folderContent: {
+      folder: "Dossier",
+      itemsUnderFolder: ({ count }) =>
+        count === 1 ? "1 élément sous ce dossier" : `${count} éléments sous ce dossier.`,
+    },
+    tagContent: {
+      tag: "Étiquette",
+      tagIndex: "Index des étiquettes",
+      itemsUnderTag: ({ count }) =>
+        count === 1 ? "1 élément avec cette étiquette" : `${count} éléments avec cette étiquette.`,
+      showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`,
+      totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`,
+    },
+  },
+} as const satisfies Translation
diff --git a/quartz/i18n/locales/fr.json b/quartz/i18n/locales/fr.json
deleted file mode 100644
index 8b526b5..0000000
--- a/quartz/i18n/locales/fr.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "404": "Soit cette page est privée, soit elle n'existe pas.",
-  "backlinks": {
-    "backlinks": "Rétroliens",
-    "noBacklinksFound": "Aucun rétrolien trouvé"
-  },
-  "common": {
-    "item": "fichier"
-  },
-  "darkmode": {
-    "darkmode": "Thème sombre",
-    "lightMode": "Thème clair"
-  },
-  "folderContent": {
-    "underThisFolder": "dans ce dossier"
-  },
-  "footer": {
-    "createdWith": "Créé avec"
-  },
-  "graph": {
-    "graphView": "Vue Graphique"
-  },
-  "head": {
-    "noDescriptionProvided": "Aucune description n'a été fournie",
-    "untitled": "Sans titre"
-  },
-  "recentNotes": {
-    "seeRemainingMore": "Voir {{remaining}} plus"
-  },
-  "search": "Rechercher",
-  "tableOfContent": "Table des Matières",
-  "tagContent": {
-    "showingFirst": "Afficher en premier",
-    "totalTags": "tags totaux",
-    "withThisTag": "avec ce tag",
-    "found": "Trouvé"
-  }
-}
diff --git a/quartz/plugins/emitters/404.tsx b/quartz/plugins/emitters/404.tsx
index 58ae59a..079adbc 100644
--- a/quartz/plugins/emitters/404.tsx
+++ b/quartz/plugins/emitters/404.tsx
@@ -8,6 +8,7 @@
 import { NotFound } from "../../components"
 import { defaultProcessedContent } from "../vfile"
 import { write } from "./helpers"
+import { i18n } from "../../i18n"
 
 export const NotFoundPage: QuartzEmitterPlugin = () => {
   const opts: FullPageLayout = {
@@ -33,11 +34,12 @@
       const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
       const path = url.pathname as FullSlug
       const externalResources = pageResources(path, resources)
+      const notFound = i18n(cfg.locale).pages.error.title
       const [tree, vfile] = defaultProcessedContent({
         slug,
-        text: "Not Found",
-        description: "Not Found",
-        frontmatter: { title: "Not Found", tags: [] },
+        text: notFound,
+        description: notFound,
+        frontmatter: { title: notFound, tags: [] },
       })
       const componentData: QuartzComponentProps = {
         fileData: vfile.data,
@@ -51,7 +53,7 @@
       return [
         await write({
           ctx,
-          content: renderPage(slug, componentData, opts, externalResources),
+          content: renderPage(cfg, slug, componentData, opts, externalResources),
           slug,
           ext: ".html",
         }),
diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts
index 5a0bed9..1c86b71 100644
--- a/quartz/plugins/emitters/contentIndex.ts
+++ b/quartz/plugins/emitters/contentIndex.ts
@@ -6,6 +6,7 @@
 import { QuartzEmitterPlugin } from "../types"
 import { toHtml } from "hast-util-to-html"
 import { write } from "./helpers"
+import { i18n } from "../../i18n"
 
 export type ContentIndex = Map<FullSlug, ContentDetails>
 export type ContentDetails = {
@@ -38,7 +39,7 @@
   const base = cfg.baseUrl ?? ""
   const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url>
     <loc>https://${joinSegments(base, encodeURI(slug))}</loc>
-    <lastmod>${content.date?.toISOString()}</lastmod>
+    ${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`}
   </url>`
   const urls = Array.from(idx)
     .map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
@@ -78,7 +79,7 @@
     <channel>
       <title>${escapeHTML(cfg.pageTitle)}</title>
       <link>https://${base}</link>
-      <description>${!!limit ? `Last ${limit} notes` : "Recent notes"} on ${escapeHTML(
+      <description>${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({ count: limit }) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML(
         cfg.pageTitle,
       )}</description>
       <generator>Quartz -- quartz.jzhao.xyz</generator>
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index f8e6404..b11890b 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -49,7 +49,7 @@
           allFiles,
         }
 
-        const content = renderPage(slug, componentData, opts, externalResources)
+        const content = renderPage(cfg, slug, componentData, opts, externalResources)
         const fp = await write({
           ctx,
           content,
diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx
index 04a5a00..35c360a 100644
--- a/quartz/plugins/emitters/folderPage.tsx
+++ b/quartz/plugins/emitters/folderPage.tsx
@@ -18,6 +18,7 @@
 import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 import { FolderContent } from "../../components"
 import { write } from "./helpers"
+import { i18n } from "../../i18n"
 
 export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
   const opts: FullPageLayout = {
@@ -57,7 +58,10 @@
           folder,
           defaultProcessedContent({
             slug: joinSegments(folder, "index") as FullSlug,
-            frontmatter: { title: `Folder: ${folder}`, tags: [] },
+            frontmatter: {
+              title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`,
+              tags: [],
+            },
           }),
         ]),
       )
@@ -82,7 +86,7 @@
           allFiles,
         }
 
-        const content = renderPage(slug, componentData, opts, externalResources)
+        const content = renderPage(cfg, slug, componentData, opts, externalResources)
         const fp = await write({
           ctx,
           content,
diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx
index 3e450f6..2411c68 100644
--- a/quartz/plugins/emitters/tagPage.tsx
+++ b/quartz/plugins/emitters/tagPage.tsx
@@ -15,6 +15,7 @@
 import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 import { TagContent } from "../../components"
 import { write } from "./helpers"
+import { i18n } from "../../i18n"
 
 export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
   const opts: FullPageLayout = {
@@ -47,7 +48,10 @@
 
       const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
         [...tags].map((tag) => {
-          const title = tag === "index" ? "Tag Index" : `Tag: #${tag}`
+          const title =
+            tag === "index"
+              ? i18n(cfg.locale).pages.tagContent.tagIndex
+              : `${i18n(cfg.locale).pages.tagContent.tag}: #${tag}`
           return [
             tag,
             defaultProcessedContent({
@@ -81,7 +85,7 @@
           allFiles,
         }
 
-        const content = renderPage(slug, componentData, opts, externalResources)
+        const content = renderPage(cfg, slug, componentData, opts, externalResources)
         const fp = await write({
           ctx,
           content,
diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts
index eae359e..9371df8 100644
--- a/quartz/plugins/transformers/frontmatter.ts
+++ b/quartz/plugins/transformers/frontmatter.ts
@@ -5,6 +5,7 @@
 import toml from "toml"
 import { slugTag } from "../../util/path"
 import { QuartzPluginData } from "../vfile"
+import { i18n } from "../../i18n"
 
 export interface Options {
   delims: string | string[]
@@ -43,7 +44,7 @@
   const opts = { ...defaultOptions, ...userOpts }
   return {
     name: "FrontMatter",
-    markdownPlugins() {
+    markdownPlugins({ cfg }) {
       return [
         [remarkFrontmatter, ["yaml", "toml"]],
         () => {
@@ -59,7 +60,7 @@
             if (data.title) {
               data.title = data.title.toString()
             } else if (data.title === null || data.title === undefined) {
-              data.title = file.stem ?? "Untitled"
+              data.title = file.stem ?? i18n(cfg.configuration.locale).propertyDefaults.title
             }
 
             const tags = coerceToArray(coalesceAliases(data, ["tags", "tag"]))
diff --git a/quartz/plugins/transformers/toc.ts b/quartz/plugins/transformers/toc.ts
index e831f94..4c31d20 100644
--- a/quartz/plugins/transformers/toc.ts
+++ b/quartz/plugins/transformers/toc.ts
@@ -3,7 +3,6 @@
 import { visit } from "unist-util-visit"
 import { toString } from "mdast-util-to-string"
 import Slugger from "github-slugger"
-import { wikilinkRegex } from "./ofm"
 
 export interface Options {
   maxDepth: 1 | 2 | 3 | 4 | 5 | 6
@@ -25,7 +24,6 @@
   slug: string // this is just the anchor (#some-slug), not the canonical slug
 }
 
-const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g")
 const slugAnchor = new Slugger()
 export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = (
   userOpts,
@@ -44,16 +42,7 @@
               let highestDepth: number = opts.maxDepth
               visit(tree, "heading", (node) => {
                 if (node.depth <= opts.maxDepth) {
-                  let text = toString(node)
-
-                  // strip link formatting from toc entries
-                  text = text.replace(wikilinkRegex, (_, rawFp, __, rawAlias) => {
-                    const fp = rawFp?.trim() ?? ""
-                    const alias = rawAlias?.slice(1).trim()
-                    return alias ?? fp
-                  })
-                  text = text.replace(regexMdLinks, "$1")
-
+                  const text = toString(node)
                   highestDepth = Math.min(highestDepth, node.depth)
                   toc.push({
                     depth: node.depth,
diff --git a/quartz/util/lang.ts b/quartz/util/lang.ts
index 31e8c02..6fb0469 100644
--- a/quartz/util/lang.ts
+++ b/quartz/util/lang.ts
@@ -1,11 +1,3 @@
-export function pluralize(count: number, s: string): string {
-  if (count === 1) {
-    return `1 ${s}`
-  } else {
-    return `${count} ${s}s`
-  }
-}
-
 export function capitalize(s: string): string {
   return s.substring(0, 1).toUpperCase() + s.substring(1)
 }

--
Gitblit v1.10.0