From dbbc672c67aa5ac0a915d22af5cf44c4e7011aae Mon Sep 17 00:00:00 2001 From: Mara-Li Date: Sun, 4 Feb 2024 04:55:24 +0100 Subject: [PATCH] feat: Adding support for i18n (closes #462) (#738) * fix: alt error mix with height/width More granular detection of alt and resize in image * fix: format * feat: init i18n * feat: add translation * style: prettier for test * fix: build-up the locale to fusion with dateLocale * style: run prettier * remove cursed file * refactor: remove i18n library and use locale way instead * format with prettier * forgot to remove test * prevent merging error * format * format * fix: allow string for locale - Check during translation if valid / existing locale - Allow to use "en" and "en-US" for example - Add fallback directly in the function - Add default key in the function - Add docstring to cfg.ts * forgot item translation * remove unused locale variable * forgot to remove fr-FR testing * format --- quartz.config.ts | 1 + quartz/cfg.ts | 2 +- quartz/components/Backlinks.tsx | 7 ++-- quartz/components/Darkmode.tsx | 7 ++-- quartz/components/Footer.tsx | 6 ++-- quartz/components/Graph.tsx | 5 +-- quartz/components/Head.tsx | 6 ++-- quartz/components/RecentNotes.tsx | 9 ++++- quartz/components/Search.tsx | 5 +-- quartz/components/TableOfContents.tsx | 10 +++--- quartz/components/pages/404.tsx | 7 ++-- quartz/components/pages/FolderContent.tsx | 8 +++-- quartz/components/pages/TagContent.tsx | 19 ++++++++--- quartz/components/scripts/search.inline.ts | 8 ++++- quartz/i18n/i18next.ts | 37 +++++++++++++++++++++ quartz/i18n/locales/en.json | 37 +++++++++++++++++++++ quartz/i18n/locales/fr.json | 38 ++++++++++++++++++++++ 17 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 quartz/i18n/i18next.ts create mode 100644 quartz/i18n/locales/en.json create mode 100644 quartz/i18n/locales/fr.json diff --git a/quartz.config.ts b/quartz.config.ts index d4fc5d38a..4921a1185 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 a7f79e3b8..e7ae783f8 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 d5bdc0b95..1688db62d 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 6d10bb99e..056e684d3 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 54440cffd..40faef9c6 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 (

- Created with Quartz v{version}, © {year} + {i18n(cfg.locale, "footer.createdWith")}{" "} + Quartz v{version}, © {year}

    {Object.entries(links).map(([text, link]) => ( diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx index 756a46bb7..f728c5e5d 100644 --- a/quartz/components/Graph.tsx +++ b/quartz/components/Graph.tsx @@ -2,6 +2,7 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types" // @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 @@ const defaultOptions: GraphOptions = { } 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 (
    -

    Graph View

    +

    {i18n(cfg.locale, "graph.graphView")}

    { 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 81b354d77..240ef98f3 100644 --- a/quartz/components/RecentNotes.tsx +++ b/quartz/components/RecentNotes.tsx @@ -5,6 +5,7 @@ import { byDateAndAlphabetical } from "./PageList" 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 @@ export default ((userOpts?: Partial) => {
{opts.linkToMore && remaining > 0 && (

- See {remaining} more → + + {" "} + {i18n(cfg.locale, "recentNotes.seeRemainingMore", { + remaining: remaining.toString(), + })}{" "} + → +

)}
diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx index 239bc033b..b73ce0bfc 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 c276f568d..56adbf981 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 47fb02f1b..02938e32c 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 ec30c5ff9..57a6c3216 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 59942ebf5..a75f4ff46 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 000000000..39c446132 --- /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 000000000..28b6dff2d --- /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 000000000..97f8f31bc --- /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é" + } +}