From dbbc672c67aa5ac0a915d22af5cf44c4e7011aae Mon Sep 17 00:00:00 2001
From: Mara-Li <mara-li@outlook.fr>
Date: Sun, 04 Feb 2024 03:55:24 +0000
Subject: [PATCH] feat: Adding support for i18n (closes #462) (#738)
---
quartz/components/RecentNotes.tsx | 9 ++
quartz/components/Footer.tsx | 6 +
quartz/components/Backlinks.tsx | 7 +
quartz/components/Darkmode.tsx | 7 +
quartz/cfg.ts | 2
quartz/components/Head.tsx | 6 +
quartz/components/Graph.tsx | 5
quartz/components/scripts/search.inline.ts | 8 +
quartz/i18n/locales/en.json | 37 +++++++++
quartz/i18n/i18next.ts | 37 +++++++++
quartz/components/TableOfContents.tsx | 10 +-
quartz/i18n/locales/fr.json | 38 +++++++++
quartz/components/pages/TagContent.tsx | 19 +++-
quartz/components/Search.tsx | 5
quartz/components/pages/FolderContent.tsx | 8 +
quartz.config.ts | 1
quartz/components/pages/404.tsx | 7 +
17 files changed, 180 insertions(+), 32 deletions(-)
diff --git a/quartz.config.ts b/quartz.config.ts
index d4fc5d3..4921a11 100644
--- a/quartz.config.ts
+++ b/quartz.config.ts
@@ -9,6 +9,7 @@
analytics: {
provider: "plausible",
},
+ locale: "en-US",
baseUrl: "quartz.jzhao.xyz",
ignorePatterns: ["private", "templates", ".obsidian"],
defaultDateType: "created",
diff --git a/quartz/cfg.ts b/quartz/cfg.ts
index a7f79e3..e7ae783 100644
--- a/quartz/cfg.ts
+++ b/quartz/cfg.ts
@@ -37,8 +37,8 @@
baseUrl?: string
theme: Theme
/**
- * The locale to use for date formatting. Default to "en-US"
* 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)
*/
locale?: string
diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx
index d5bdc0b..1688db6 100644
--- a/quartz/components/Backlinks.tsx
+++ b/quartz/components/Backlinks.tsx
@@ -1,14 +1,15 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/backlinks.scss"
import { resolveRelative, simplifySlug } from "../util/path"
+import { i18n } from "../i18n/i18next"
import { classNames } from "../util/lang"
-function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
+function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentProps) {
const slug = simplifySlug(fileData.slug!)
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
return (
<div class={classNames(displayClass, "backlinks")}>
- <h3>Backlinks</h3>
+ <h3>{i18n(cfg.locale, "backlinks.backlinks")}</h3>
<ul class="overflow">
{backlinkFiles.length > 0 ? (
backlinkFiles.map((f) => (
@@ -19,7 +20,7 @@
</li>
))
) : (
- <li>No backlinks found</li>
+ <li>{i18n(cfg.locale, "backlinks.noBlacklinksFound")}</li>
)}
</ul>
</div>
diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx
index 6d10bb9..056e684 100644
--- a/quartz/components/Darkmode.tsx
+++ b/quartz/components/Darkmode.tsx
@@ -4,9 +4,10 @@
import darkmodeScript from "./scripts/darkmode.inline"
import styles from "./styles/darkmode.scss"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
+import { i18n } from "../i18n/i18next"
import { classNames } from "../util/lang"
-function Darkmode({ displayClass }: QuartzComponentProps) {
+function Darkmode({ displayClass, cfg }: QuartzComponentProps) {
return (
<div class={classNames(displayClass, "darkmode")}>
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
@@ -22,7 +23,7 @@
style="enable-background:new 0 0 35 35"
xmlSpace="preserve"
>
- <title>Light mode</title>
+ <title>{i18n(cfg.locale, "darkmode.lightMode")}</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>
@@ -38,7 +39,7 @@
style="enable-background:new 0 0 100 100"
xmlSpace="preserve"
>
- <title>Dark mode</title>
+ <title>{i18n(cfg.locale, "darkmode.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/Footer.tsx b/quartz/components/Footer.tsx
index 54440cf..40faef9 100644
--- a/quartz/components/Footer.tsx
+++ b/quartz/components/Footer.tsx
@@ -1,20 +1,22 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/footer.scss"
import { version } from "../../package.json"
+import { i18n } from "../i18n/i18next"
interface Options {
links: Record<string, string>
}
export default ((opts?: Options) => {
- function Footer({ displayClass }: QuartzComponentProps) {
+ function Footer({ displayClass, cfg }: QuartzComponentProps) {
const year = new Date().getFullYear()
const links = opts?.links ?? []
return (
<footer class={`${displayClass ?? ""}`}>
<hr />
<p>
- Created with <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year}
+ {i18n(cfg.locale, "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 756a46b..f728c5e 100644
--- a/quartz/components/Graph.tsx
+++ b/quartz/components/Graph.tsx
@@ -2,6 +2,7 @@
// @ts-ignore
import script from "./scripts/graph.inline"
import style from "./styles/graph.scss"
+import { i18n } from "../i18n/i18next"
import { classNames } from "../util/lang"
export interface D3Config {
@@ -53,12 +54,12 @@
}
export default ((opts?: GraphOptions) => {
- function Graph({ displayClass }: QuartzComponentProps) {
+ function Graph({ displayClass, cfg }: QuartzComponentProps) {
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
return (
<div class={classNames(displayClass, "graph")}>
- <h3>Graph View</h3>
+ <h3>{i18n(cfg.locale, "graph.graphView")}</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 2bf2638..b49c385 100644
--- a/quartz/components/Head.tsx
+++ b/quartz/components/Head.tsx
@@ -1,11 +1,13 @@
+import { i18n } from "../i18n/i18next"
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 ?? "Untitled"
- const description = fileData.description?.trim() ?? "No description provided"
+ const title = fileData.frontmatter?.title ?? i18n(cfg.locale, "head.untitled")
+ const description =
+ fileData.description?.trim() ?? i18n(cfg.locale, "head.noDescriptionProvided")
const { css, js } = externalResources
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx
index 81b354d..240ef98 100644
--- a/quartz/components/RecentNotes.tsx
+++ b/quartz/components/RecentNotes.tsx
@@ -5,6 +5,7 @@
import style from "./styles/recentNotes.scss"
import { Date, getDate } from "./Date"
import { GlobalConfiguration } from "../cfg"
+import { i18n } from "../i18n/i18next"
import { classNames } from "../util/lang"
interface Options {
@@ -70,7 +71,13 @@
</ul>
{opts.linkToMore && remaining > 0 && (
<p>
- <a href={resolveRelative(fileData.slug!, opts.linkToMore)}>See {remaining} more →</a>
+ <a href={resolveRelative(fileData.slug!, opts.linkToMore)}>
+ {" "}
+ {i18n(cfg.locale, "recentNotes.seeRemainingMore", {
+ remaining: remaining.toString(),
+ })}{" "}
+ →
+ </a>
</p>
)}
</div>
diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx
index 239bc03..b73ce0b 100644
--- a/quartz/components/Search.tsx
+++ b/quartz/components/Search.tsx
@@ -3,6 +3,7 @@
// @ts-ignore
import script from "./scripts/search.inline"
import { classNames } from "../util/lang"
+import { i18n } from "../i18n/i18next"
export interface SearchOptions {
enablePreview: boolean
@@ -13,13 +14,13 @@
}
export default ((userOpts?: Partial<SearchOptions>) => {
- function Search({ displayClass }: QuartzComponentProps) {
+ function Search({ displayClass, cfg }: QuartzComponentProps) {
const opts = { ...defaultOptions, ...userOpts }
return (
<div class={classNames(displayClass, "search")}>
<div id="search-icon">
- <p>Search</p>
+ <p>{i18n(cfg.locale, "search")}</p>
<div></div>
<svg
tabIndex={0}
diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx
index 167c837..2e01507 100644
--- a/quartz/components/TableOfContents.tsx
+++ b/quartz/components/TableOfContents.tsx
@@ -5,6 +5,7 @@
// @ts-ignore
import script from "./scripts/toc.inline"
+import { i18n } from "../i18n/i18next"
interface Options {
layout: "modern" | "legacy"
@@ -14,7 +15,7 @@
layout: "modern",
}
-function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
+function TableOfContents({ fileData, displayClass, cfg }: QuartzComponentProps) {
if (!fileData.toc) {
return null
}
@@ -22,7 +23,7 @@
return (
<div class={classNames(displayClass, "toc")}>
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}>
- <h3>Table of Contents</h3>
+ <h3>{i18n(cfg.locale, "tableOfContent")}</h3>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@@ -55,15 +56,14 @@
TableOfContents.css = modernStyle
TableOfContents.afterDOMLoaded = script
-function LegacyTableOfContents({ fileData }: QuartzComponentProps) {
+function LegacyTableOfContents({ fileData, cfg }: QuartzComponentProps) {
if (!fileData.toc) {
return null
}
-
return (
<details id="toc" open={!fileData.collapseToc}>
<summary>
- <h3>Table of Contents</h3>
+ <h3>{i18n(cfg.locale, "tableOfContent")}</h3>
</summary>
<ul>
{fileData.toc.map((tocEntry) => (
diff --git a/quartz/components/pages/404.tsx b/quartz/components/pages/404.tsx
index c276f56..56adbf9 100644
--- a/quartz/components/pages/404.tsx
+++ b/quartz/components/pages/404.tsx
@@ -1,10 +1,11 @@
-import { QuartzComponentConstructor } from "../types"
+import { i18n } from "../../i18n/i18next"
+import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
-function NotFound() {
+function NotFound({ cfg }: QuartzComponentProps) {
return (
<article class="popover-hint">
<h1>404</h1>
- <p>Either this page is private or doesn't exist.</p>
+ <p>{i18n(cfg.locale, "404")}</p>
</article>
)
}
diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx
index 47fb02f..02938e3 100644
--- a/quartz/components/pages/FolderContent.tsx
+++ b/quartz/components/pages/FolderContent.tsx
@@ -7,6 +7,7 @@
import { Root } from "hast"
import { pluralize } from "../../util/lang"
import { htmlToJsx } from "../../util/jsx"
+import { i18n } from "../../i18n/i18next"
interface FolderContentOptions {
/**
@@ -23,7 +24,7 @@
const options: FolderContentOptions = { ...defaultOptions, ...opts }
function FolderContent(props: QuartzComponentProps) {
- const { tree, fileData, allFiles } = props
+ const { tree, fileData, allFiles, cfg } = props
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!))
const allPagesInFolder = allFiles.filter((file) => {
const fileSlug = _stripSlashes(simplifySlug(file.slug!))
@@ -52,7 +53,10 @@
</article>
<div class="page-listing">
{options.showFolderCount && (
- <p>{pluralize(allPagesInFolder.length, "item")} under this folder.</p>
+ <p>
+ {pluralize(allPagesInFolder.length, i18n(cfg.locale, "common.item"))}{" "}
+ {i18n(cfg.locale, "folderContent.underThisFolder")}.
+ </p>
)}
<div>
<PageList {...listProps} />
diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx
index ec30c5f..57a6c32 100644
--- a/quartz/components/pages/TagContent.tsx
+++ b/quartz/components/pages/TagContent.tsx
@@ -6,10 +6,11 @@
import { Root } from "hast"
import { pluralize } from "../../util/lang"
import { htmlToJsx } from "../../util/jsx"
+import { i18n } from "../../i18n/i18next"
const numPages = 10
function TagContent(props: QuartzComponentProps) {
- const { tree, fileData, allFiles } = props
+ const { tree, fileData, allFiles, cfg } = props
const slug = fileData.slug
if (!(slug?.startsWith("tags/") || slug === "tags")) {
@@ -43,7 +44,10 @@
<article>
<p>{content}</p>
</article>
- <p>Found {tags.length} total tags.</p>
+ <p>
+ {i18n(cfg.locale, "tagContent.found")} {tags.length}{" "}
+ {i18n(cfg.locale, "tagContent.totalTags")}.
+ </p>
<div>
{tags.map((tag) => {
const pages = tagItemMap.get(tag)!
@@ -64,8 +68,10 @@
{content && <p>{content}</p>}
<div class="page-listing">
<p>
- {pluralize(pages.length, "item")} with this tag.{" "}
- {pages.length > numPages && `Showing first ${numPages}.`}
+ {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "}
+ {i18n(cfg.locale, "tagContent.withThisTag")}.{" "}
+ {pages.length > numPages &&
+ `${i18n(cfg.locale, "tagContent.showingFirst")} ${numPages}.`}
</p>
<PageList limit={numPages} {...listProps} />
</div>
@@ -86,7 +92,10 @@
<div class={classes}>
<article>{content}</article>
<div class="page-listing">
- <p>{pluralize(pages.length, "item")} with this tag.</p>
+ <p>
+ {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "}
+ {i18n(cfg.locale, "tagContent.withThisTag")}.
+ </p>
<div>
<PageList {...listProps} />
</div>
diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts
index 59942eb..a75f4ff 100644
--- a/quartz/components/scripts/search.inline.ts
+++ b/quartz/components/scripts/search.inline.ts
@@ -306,7 +306,13 @@
itemTile.classList.add("result-card")
itemTile.id = slug
itemTile.href = resolveUrl(slug).toString()
- itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p class="preview">${content}</p>`
+ itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}${
+ enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`
+ }`
+ itemTile.addEventListener("click", (event) => {
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
+ hideSearch()
+ })
const handler = (event: MouseEvent) => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
diff --git a/quartz/i18n/i18next.ts b/quartz/i18n/i18next.ts
new file mode 100644
index 0000000..39c4461
--- /dev/null
+++ b/quartz/i18n/i18next.ts
@@ -0,0 +1,37 @@
+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/locales/en.json b/quartz/i18n/locales/en.json
new file mode 100644
index 0000000..28b6dff
--- /dev/null
+++ b/quartz/i18n/locales/en.json
@@ -0,0 +1,37 @@
+{
+ "404": "Either this page is private or doesn't exist.",
+ "backlinks": {
+ "backlinks": "Backlinks",
+ "noBlacklinksFound": "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.json b/quartz/i18n/locales/fr.json
new file mode 100644
index 0000000..97f8f31
--- /dev/null
+++ b/quartz/i18n/locales/fr.json
@@ -0,0 +1,38 @@
+{
+ "404": "Soit cette page est privée, soit elle n'existe pas.",
+ "backlinks": {
+ "backlinks": "Rétroliens",
+ "noBlacklinksFound": "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é"
+ }
+}
--
Gitblit v1.10.0