diff --git a/quartz.config.ts b/quartz.config.ts index d4fc5d38..4921a118 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -9,6 +9,7 @@ const config: QuartzConfig = { 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 a7f79e3b..e7ae783f 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -37,8 +37,8 @@ export interface GlobalConfiguration { 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 d5bdc0b9..1688db62 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 (
-

Backlinks

+

{i18n(cfg.locale, "backlinks.backlinks")}

diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx index 6d10bb99..056e684d 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 (
@@ -22,7 +23,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) { style="enable-background:new 0 0 35 35" xmlSpace="preserve" > - Light mode + {i18n(cfg.locale, "darkmode.lightMode")} @@ -38,7 +39,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) { style="enable-background:new 0 0 100 100" xmlSpace="preserve" > - Dark mode + {i18n(cfg.locale, "darkmode.lightMode")} diff --git a/quartz/components/Footer.tsx b/quartz/components/Footer.tsx index 54440cff..40faef9c 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 } export default ((opts?: Options) => { - function Footer({ displayClass }: QuartzComponentProps) { + function Footer({ displayClass, cfg }: QuartzComponentProps) { const year = new Date().getFullYear() const links = opts?.links ?? [] return (
diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx index 239bc033..b73ce0bf 100644 --- a/quartz/components/Search.tsx +++ b/quartz/components/Search.tsx @@ -3,6 +3,7 @@ import style from "./styles/search.scss" // @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 @@ const defaultOptions: SearchOptions = { } export default ((userOpts?: Partial) => { - function Search({ displayClass }: QuartzComponentProps) { + function Search({ displayClass, cfg }: QuartzComponentProps) { const opts = { ...defaultOptions, ...userOpts } return (
-

Search

+

{i18n(cfg.locale, "search")}

Table of Contents

+

{i18n(cfg.locale, "tableOfContent")}

-

Table of Contents

+

{i18n(cfg.locale, "tableOfContent")}

    {fileData.toc.map((tocEntry) => ( diff --git a/quartz/components/pages/404.tsx b/quartz/components/pages/404.tsx index c276f568..56adbf98 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 (

    404

    -

    Either this page is private or doesn't exist.

    +

    {i18n(cfg.locale, "404")}

    ) } diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index 47fb02f1..02938e32 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -7,6 +7,7 @@ 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" interface FolderContentOptions { /** @@ -23,7 +24,7 @@ export default ((opts?: Partial) => { 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 @@ export default ((opts?: Partial) => {
    {options.showFolderCount && ( -

    {pluralize(allPagesInFolder.length, "item")} under this folder.

    +

    + {pluralize(allPagesInFolder.length, i18n(cfg.locale, "common.item"))}{" "} + {i18n(cfg.locale, "folderContent.underThisFolder")}. +

    )}
    diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx index ec30c5ff..57a6c321 100644 --- a/quartz/components/pages/TagContent.tsx +++ b/quartz/components/pages/TagContent.tsx @@ -6,10 +6,11 @@ import { QuartzPluginData } from "../../plugins/vfile" 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 @@ function TagContent(props: QuartzComponentProps) {

    {content}

    -

    Found {tags.length} total tags.

    +

    + {i18n(cfg.locale, "tagContent.found")} {tags.length}{" "} + {i18n(cfg.locale, "tagContent.totalTags")}. +

    {tags.map((tag) => { const pages = tagItemMap.get(tag)! @@ -64,8 +68,10 @@ function TagContent(props: QuartzComponentProps) { {content &&

    {content}

    }

    - {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}.`}

    @@ -86,7 +92,10 @@ function TagContent(props: QuartzComponentProps) {
    {content}
    -

    {pluralize(pages.length, "item")} with this tag.

    +

    + {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "} + {i18n(cfg.locale, "tagContent.withThisTag")}. +

    diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 59942ebf..a75f4ff4 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -306,7 +306,13 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { itemTile.classList.add("result-card") itemTile.id = slug itemTile.href = resolveUrl(slug).toString() - itemTile.innerHTML = `

    ${title}

    ${htmlTags}

    ${content}

    ` + itemTile.innerHTML = `

    ${title}

    ${htmlTags}${ + enablePreview && window.innerWidth > 600 ? "" : `

    ${content}

    ` + }` + 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 00000000..39c44613 --- /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 = + 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 00000000..28b6dff2 --- /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 00000000..97f8f31b --- /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é" + } +}