The Trans Directory
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore(i18n): refactor and cleanup (#805)

* checkpoint

* finish

* docs

authored by

Jacky Zhao and committed by
GitHub
36e4cc41 dff4b063

+326 -211
+1 -1
docs/advanced/making plugins.md
··· 278 278 allFiles, 279 279 } 280 280 281 - const content = renderPage(slug, componentData, opts, externalResources) 281 + const content = renderPage(cfg, slug, componentData, opts, externalResources) 282 282 const fp = await emit({ 283 283 content, 284 284 slug: file.data.slug!,
+1
docs/configuration.md
··· 27 27 - `null`: don't use analytics; 28 28 - `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or 29 29 - `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics 30 + - `locale`: used for [[i18n]] and date formatting 30 31 - `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. 31 32 - 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` 32 33 - 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.
+18
docs/features/i18n.md
··· 1 + --- 2 + title: Internationalization 3 + --- 4 + 5 + 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`. 6 + 7 + The locale field generally follows a certain format: `{language}-{REGION}` 8 + 9 + - `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). 10 + - `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) 11 + 12 + > [!tip] Interested in contributing? 13 + > We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v4/quartz/i18n/locales)! To contribute a translation, do the following things: 14 + > 15 + > 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file. 16 + > 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above. 17 + > 3. Fill in the translations! 18 + > 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`.
+1 -1
docs/index.md
··· 31 31 32 32 ## 🔧 Features 33 33 34 - - [[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 34 + - [[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 35 35 - Hot-reload for both configuration and content 36 36 - Simple JSX layouts and [[creating components|page components]] 37 37 - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
+6 -2
quartz/cfg.ts
··· 1 1 import { ValidDateType } from "./components/Date" 2 2 import { QuartzComponent } from "./components/types" 3 + import { ValidLocale } from "./i18n" 3 4 import { PluginTypes } from "./plugins/types" 4 5 import { Theme } from "./util/theme" 5 6 ··· 39 40 /** 40 41 * Allow to translate the date in the language of your choice. 41 42 * Also used for UI translation (default: en-US) 42 - * Need to be formated following the IETF language tag format (https://en.wikipedia.org/wiki/IETF_language_tag) 43 + * Need to be formated following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag 44 + * The first part is the language (en) and the second part is the script/region (US) 45 + * Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes 46 + * Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 43 47 */ 44 - locale?: string 48 + locale: ValidLocale 45 49 } 46 50 47 51 export interface QuartzConfig {
+1
quartz/components/ArticleTitle.tsx
··· 9 9 return null 10 10 } 11 11 } 12 + 12 13 ArticleTitle.css = ` 13 14 .article-title { 14 15 margin: 2rem 0 0 0;
+3 -3
quartz/components/Backlinks.tsx
··· 1 1 import { QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 2 import style from "./styles/backlinks.scss" 3 3 import { resolveRelative, simplifySlug } from "../util/path" 4 - import { i18n } from "../i18n/i18next" 4 + import { i18n } from "../i18n" 5 5 import { classNames } from "../util/lang" 6 6 7 7 function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentProps) { ··· 9 9 const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) 10 10 return ( 11 11 <div class={classNames(displayClass, "backlinks")}> 12 - <h3>{i18n(cfg.locale, "backlinks.backlinks")}</h3> 12 + <h3>{i18n(cfg.locale).components.backlinks.title}</h3> 13 13 <ul class="overflow"> 14 14 {backlinkFiles.length > 0 ? ( 15 15 backlinkFiles.map((f) => ( ··· 20 20 </li> 21 21 )) 22 22 ) : ( 23 - <li>{i18n(cfg.locale, "backlinks.noBacklinksFound")}</li> 23 + <li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li> 24 24 )} 25 25 </ul> 26 26 </div>
+3 -3
quartz/components/Darkmode.tsx
··· 4 4 import darkmodeScript from "./scripts/darkmode.inline" 5 5 import styles from "./styles/darkmode.scss" 6 6 import { QuartzComponentConstructor, QuartzComponentProps } from "./types" 7 - import { i18n } from "../i18n/i18next" 7 + import { i18n } from "../i18n" 8 8 import { classNames } from "../util/lang" 9 9 10 10 function Darkmode({ displayClass, cfg }: QuartzComponentProps) { ··· 23 23 style="enable-background:new 0 0 35 35" 24 24 xmlSpace="preserve" 25 25 > 26 - <title>{i18n(cfg.locale, "darkmode.lightMode")}</title> 26 + <title>{i18n(cfg.locale).components.themeToggle.darkMode}</title> 27 27 <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> 28 28 </svg> 29 29 </label> ··· 39 39 style="enable-background:new 0 0 100 100" 40 40 xmlSpace="preserve" 41 41 > 42 - <title>{i18n(cfg.locale, "darkmode.lightMode")}</title> 42 + <title>{i18n(cfg.locale).components.themeToggle.lightMode}</title> 43 43 <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> 44 44 </svg> 45 45 </label>
+3 -2
quartz/components/Date.tsx
··· 1 1 import { GlobalConfiguration } from "../cfg" 2 + import { ValidLocale } from "../i18n" 2 3 import { QuartzPluginData } from "../plugins/vfile" 3 4 4 5 interface Props { 5 6 date: Date 6 - locale?: string 7 + locale?: ValidLocale 7 8 } 8 9 9 10 export type ValidDateType = keyof Required<QuartzPluginData>["dates"] ··· 17 18 return data.dates?.[cfg.defaultDateType] 18 19 } 19 20 20 - export function formatDate(d: Date, locale = "en-US"): string { 21 + export function formatDate(d: Date, locale: ValidLocale = "en-US"): string { 21 22 return d.toLocaleDateString(locale, { 22 23 year: "numeric", 23 24 month: "short",
+3 -3
quartz/components/Explorer.tsx
··· 6 6 import { ExplorerNode, FileNode, Options } from "./ExplorerNode" 7 7 import { QuartzPluginData } from "../plugins/vfile" 8 8 import { classNames } from "../util/lang" 9 + import { i18n } from "../i18n" 9 10 10 11 // Options interface defined in `ExplorerNode` to avoid circular dependency 11 12 const defaultOptions = { 12 - title: "Explorer", 13 13 folderClickBehavior: "collapse", 14 14 folderDefaultState: "collapsed", 15 15 useSavedState: true, ··· 75 75 jsonTree = JSON.stringify(folders) 76 76 } 77 77 78 - function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { 78 + function Explorer({ cfg, allFiles, displayClass, fileData }: QuartzComponentProps) { 79 79 constructFileTree(allFiles) 80 80 return ( 81 81 <div class={classNames(displayClass, "explorer")}> ··· 87 87 data-savestate={opts.useSavedState} 88 88 data-tree={jsonTree} 89 89 > 90 - <h1>{opts.title}</h1> 90 + <h1>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h1> 91 91 <svg 92 92 xmlns="http://www.w3.org/2000/svg" 93 93 width="14"
+1 -1
quartz/components/ExplorerNode.tsx
··· 12 12 type OrderEntries = "sort" | "filter" | "map" 13 13 14 14 export interface Options { 15 - title: string 15 + title?: string 16 16 folderDefaultState: "collapsed" | "open" 17 17 folderClickBehavior: "collapse" | "link" 18 18 useSavedState: boolean
+3 -3
quartz/components/Footer.tsx
··· 1 1 import { QuartzComponentConstructor, QuartzComponentProps } from "./types" 2 2 import style from "./styles/footer.scss" 3 3 import { version } from "../../package.json" 4 - import { i18n } from "../i18n/i18next" 4 + import { i18n } from "../i18n" 5 5 6 6 interface Options { 7 7 links: Record<string, string> ··· 15 15 <footer class={`${displayClass ?? ""}`}> 16 16 <hr /> 17 17 <p> 18 - {i18n(cfg.locale, "footer.createdWith")}{" "} 19 - <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a>, © {year} 18 + {i18n(cfg.locale).components.footer.createdWith}{" "} 19 + <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year} 20 20 </p> 21 21 <ul> 22 22 {Object.entries(links).map(([text, link]) => (
+2 -2
quartz/components/Graph.tsx
··· 2 2 // @ts-ignore 3 3 import script from "./scripts/graph.inline" 4 4 import style from "./styles/graph.scss" 5 - import { i18n } from "../i18n/i18next" 5 + import { i18n } from "../i18n" 6 6 import { classNames } from "../util/lang" 7 7 8 8 export interface D3Config { ··· 59 59 const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } 60 60 return ( 61 61 <div class={classNames(displayClass, "graph")}> 62 - <h3>{i18n(cfg.locale, "graph.graphView")}</h3> 62 + <h3>{i18n(cfg.locale).components.graph.title}</h3> 63 63 <div class="graph-outer"> 64 64 <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div> 65 65 <svg
+3 -3
quartz/components/Head.tsx
··· 1 - import { i18n } from "../i18n/i18next" 1 + import { i18n } from "../i18n" 2 2 import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path" 3 3 import { JSResourceToScriptElement } from "../util/resources" 4 4 import { QuartzComponentConstructor, QuartzComponentProps } from "./types" 5 5 6 6 export default (() => { 7 7 function Head({ cfg, fileData, externalResources }: QuartzComponentProps) { 8 - const title = fileData.frontmatter?.title ?? i18n(cfg.locale, "head.untitled") 8 + const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title 9 9 const description = 10 - fileData.description?.trim() ?? i18n(cfg.locale, "head.noDescriptionProvided") 10 + fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description 11 11 const { css, js } = externalResources 12 12 13 13 const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
+2 -1
quartz/components/PageTitle.tsx
··· 1 1 import { pathToRoot } from "../util/path" 2 2 import { QuartzComponentConstructor, QuartzComponentProps } from "./types" 3 3 import { classNames } from "../util/lang" 4 + import { i18n } from "../i18n" 4 5 5 6 function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) { 6 - const title = cfg?.pageTitle ?? "Untitled Quartz" 7 + const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title 7 8 const baseDir = pathToRoot(fileData.slug!) 8 9 return ( 9 10 <h1 class={classNames(displayClass, "page-title")}>
+5 -10
quartz/components/RecentNotes.tsx
··· 5 5 import style from "./styles/recentNotes.scss" 6 6 import { Date, getDate } from "./Date" 7 7 import { GlobalConfiguration } from "../cfg" 8 - import { i18n } from "../i18n/i18next" 8 + import { i18n } from "../i18n" 9 9 import { classNames } from "../util/lang" 10 10 11 11 interface Options { 12 - title: string 12 + title?: string 13 13 limit: number 14 14 linkToMore: SimpleSlug | false 15 15 filter: (f: QuartzPluginData) => boolean ··· 17 17 } 18 18 19 19 const defaultOptions = (cfg: GlobalConfiguration): Options => ({ 20 - title: "Recent Notes", 21 20 limit: 3, 22 21 linkToMore: false, 23 22 filter: () => true, ··· 31 30 const remaining = Math.max(0, pages.length - opts.limit) 32 31 return ( 33 32 <div class={classNames(displayClass, "recent-notes")}> 34 - <h3>{opts.title}</h3> 33 + <h3>{opts.title ?? i18n(cfg.locale).components.recentNotes.title}</h3> 35 34 <ul class="recent-ul"> 36 35 {pages.slice(0, opts.limit).map((page) => { 37 - const title = page.frontmatter?.title 36 + const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title 38 37 const tags = page.frontmatter?.tags ?? [] 39 38 40 39 return ( ··· 72 71 {opts.linkToMore && remaining > 0 && ( 73 72 <p> 74 73 <a href={resolveRelative(fileData.slug!, opts.linkToMore)}> 75 - {" "} 76 - {i18n(cfg.locale, "recentNotes.seeRemainingMore", { 77 - remaining: remaining.toString(), 78 - })}{" "} 79 - 74 + {i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })} 80 75 </a> 81 76 </p> 82 77 )}
+5 -5
quartz/components/Search.tsx
··· 3 3 // @ts-ignore 4 4 import script from "./scripts/search.inline" 5 5 import { classNames } from "../util/lang" 6 - import { i18n } from "../i18n/i18next" 6 + import { i18n } from "../i18n" 7 7 8 8 export interface SearchOptions { 9 9 enablePreview: boolean ··· 16 16 export default ((userOpts?: Partial<SearchOptions>) => { 17 17 function Search({ displayClass, cfg }: QuartzComponentProps) { 18 18 const opts = { ...defaultOptions, ...userOpts } 19 - 19 + const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder 20 20 return ( 21 21 <div class={classNames(displayClass, "search")}> 22 22 <div id="search-icon"> 23 - <p>{i18n(cfg.locale, "search")}</p> 23 + <p>{i18n(cfg.locale).components.search.title}</p> 24 24 <div></div> 25 25 <svg 26 26 tabIndex={0} ··· 44 44 id="search-bar" 45 45 name="search" 46 46 type="text" 47 - aria-label="Search for something" 48 - placeholder="Search for something" 47 + aria-label={searchPlaceholder} 48 + placeholder={searchPlaceholder} 49 49 /> 50 50 <div id="search-layout" data-preview={opts.enablePreview}></div> 51 51 </div>
+3 -3
quartz/components/TableOfContents.tsx
··· 5 5 6 6 // @ts-ignore 7 7 import script from "./scripts/toc.inline" 8 - import { i18n } from "../i18n/i18next" 8 + import { i18n } from "../i18n" 9 9 10 10 interface Options { 11 11 layout: "modern" | "legacy" ··· 23 23 return ( 24 24 <div class={classNames(displayClass, "toc")}> 25 25 <button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}> 26 - <h3>{i18n(cfg.locale, "tableOfContent")}</h3> 26 + <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> 27 27 <svg 28 28 xmlns="http://www.w3.org/2000/svg" 29 29 width="24" ··· 63 63 return ( 64 64 <details id="toc" open={!fileData.collapseToc}> 65 65 <summary> 66 - <h3>{i18n(cfg.locale, "tableOfContent")}</h3> 66 + <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> 67 67 </summary> 68 68 <ul> 69 69 {fileData.toc.map((tocEntry) => (
+2 -2
quartz/components/pages/404.tsx
··· 1 - import { i18n } from "../../i18n/i18next" 1 + import { i18n } from "../../i18n" 2 2 import { QuartzComponentConstructor, QuartzComponentProps } from "../types" 3 3 4 4 function NotFound({ cfg }: QuartzComponentProps) { 5 5 return ( 6 6 <article class="popover-hint"> 7 7 <h1>404</h1> 8 - <p>{i18n(cfg.locale, "404")}</p> 8 + <p>{i18n(cfg.locale).pages.error.notFound}</p> 9 9 </article> 10 10 ) 11 11 }
+4 -4
quartz/components/pages/FolderContent.tsx
··· 5 5 import { PageList } from "../PageList" 6 6 import { _stripSlashes, simplifySlug } from "../../util/path" 7 7 import { Root } from "hast" 8 - import { pluralize } from "../../util/lang" 9 8 import { htmlToJsx } from "../../util/jsx" 10 - import { i18n } from "../../i18n/i18next" 9 + import { i18n } from "../../i18n" 11 10 12 11 interface FolderContentOptions { 13 12 /** ··· 54 53 <div class="page-listing"> 55 54 {options.showFolderCount && ( 56 55 <p> 57 - {pluralize(allPagesInFolder.length, i18n(cfg.locale, "common.item"))}{" "} 58 - {i18n(cfg.locale, "folderContent.underThisFolder")}. 56 + {i18n(cfg.locale).pages.folderContent.itemsUnderFolder({ 57 + count: allPagesInFolder.length, 58 + })} 59 59 </p> 60 60 )} 61 61 <div>
+9 -14
quartz/components/pages/TagContent.tsx
··· 4 4 import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" 5 5 import { QuartzPluginData } from "../../plugins/vfile" 6 6 import { Root } from "hast" 7 - import { pluralize } from "../../util/lang" 8 7 import { htmlToJsx } from "../../util/jsx" 9 - import { i18n } from "../../i18n/i18next" 8 + import { i18n } from "../../i18n" 10 9 11 10 const numPages = 10 12 11 function TagContent(props: QuartzComponentProps) { ··· 44 43 <article> 45 44 <p>{content}</p> 46 45 </article> 47 - <p> 48 - {i18n(cfg.locale, "tagContent.found")} {tags.length}{" "} 49 - {i18n(cfg.locale, "tagContent.totalTags")}. 50 - </p> 46 + <p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p> 51 47 <div> 52 48 {tags.map((tag) => { 53 49 const pages = tagItemMap.get(tag)! ··· 68 64 {content && <p>{content}</p>} 69 65 <div class="page-listing"> 70 66 <p> 71 - {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "} 72 - {i18n(cfg.locale, "tagContent.withThisTag")}.{" "} 73 - {pages.length > numPages && 74 - `${i18n(cfg.locale, "tagContent.showingFirst")} ${numPages}.`} 67 + {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })} 68 + {pages.length > numPages && ( 69 + <span> 70 + {i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })} 71 + </span> 72 + )} 75 73 </p> 76 74 <PageList limit={numPages} {...listProps} /> 77 75 </div> ··· 92 90 <div class={classes}> 93 91 <article>{content}</article> 94 92 <div class="page-listing"> 95 - <p> 96 - {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "} 97 - {i18n(cfg.locale, "tagContent.withThisTag")}. 98 - </p> 93 + <p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p> 99 94 <div> 100 95 <PageList {...listProps} /> 101 96 </div>
+17 -3
quartz/components/renderPage.tsx
··· 7 7 import { visit } from "unist-util-visit" 8 8 import { Root, Element, ElementContent } from "hast" 9 9 import { QuartzPluginData } from "../plugins/vfile" 10 + import { GlobalConfiguration } from "../cfg" 11 + import { i18n } from "../i18n" 10 12 11 13 interface RenderComponents { 12 14 head: QuartzComponent ··· 63 65 } 64 66 65 67 export function renderPage( 68 + cfg: GlobalConfiguration, 66 69 slug: FullSlug, 67 70 componentData: QuartzComponentProps, 68 71 components: RenderComponents, ··· 136 139 type: "element", 137 140 tagName: "a", 138 141 properties: { href: inner.properties?.href, class: ["internal"] }, 139 - children: [{ type: "text", value: `Link to original` }], 142 + children: [ 143 + { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, 144 + ], 140 145 }, 141 146 ] 142 147 } else if (page.htmlAst) { ··· 147 152 tagName: "h1", 148 153 properties: {}, 149 154 children: [ 150 - { type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` }, 155 + { 156 + type: "text", 157 + value: 158 + page.frontmatter?.title ?? 159 + i18n(cfg.locale).components.transcludes.transcludeOf({ 160 + targetSlug: page.slug!, 161 + }), 162 + }, 151 163 ], 152 164 }, 153 165 ...(page.htmlAst.children as ElementContent[]).map((child) => ··· 157 169 type: "element", 158 170 tagName: "a", 159 171 properties: { href: inner.properties?.href, class: ["internal"] }, 160 - children: [{ type: "text", value: `Link to original` }], 172 + children: [ 173 + { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, 174 + ], 161 175 }, 162 176 ] 163 177 }
-37
quartz/i18n/i18next.ts
··· 1 - import en from "./locales/en.json" 2 - import fr from "./locales/fr.json" 3 - 4 - const TRANSLATION = { 5 - "en-US": en, 6 - "fr-FR": fr, 7 - } as const 8 - 9 - type TranslationOptions = { 10 - [key: string]: string 11 - } 12 - 13 - export const i18n = (lang = "en-US", key: string, options?: TranslationOptions) => { 14 - const locale = 15 - Object.keys(TRANSLATION).find( 16 - (key) => 17 - key.toLowerCase() === lang.toLowerCase() || key.toLowerCase().includes(lang.toLowerCase()), 18 - ) ?? "en-US" 19 - const getTranslation = (key: string) => { 20 - const keys = key.split(".") 21 - let translationString: string | Record<string, unknown> = 22 - TRANSLATION[locale as keyof typeof TRANSLATION] 23 - keys.forEach((key) => { 24 - // @ts-ignore 25 - translationString = translationString[key] 26 - }) 27 - return translationString 28 - } 29 - if (options) { 30 - let translationString = getTranslation(key).toString() 31 - Object.keys(options).forEach((key) => { 32 - translationString = translationString.replace(`{{${key}}}`, options[key]) 33 - }) 34 - return translationString 35 - } 36 - return getTranslation(key).toString() 37 - }
+11
quartz/i18n/index.ts
··· 1 + import { Translation } from "./locales/definition" 2 + import en from "./locales/en-US" 3 + import fr from "./locales/fr-FR" 4 + 5 + export const TRANSLATIONS = { 6 + "en-US": en, 7 + "fr-FR": fr, 8 + } as const 9 + 10 + export const i18n = (locale: ValidLocale): Translation => TRANSLATIONS[locale] 11 + export type ValidLocale = keyof typeof TRANSLATIONS
+63
quartz/i18n/locales/definition.ts
··· 1 + import { FullSlug } from "../../util/path" 2 + 3 + export interface Translation { 4 + propertyDefaults: { 5 + title: string 6 + description: string 7 + } 8 + components: { 9 + backlinks: { 10 + title: string 11 + noBacklinksFound: string 12 + } 13 + themeToggle: { 14 + lightMode: string 15 + darkMode: string 16 + } 17 + explorer: { 18 + title: string 19 + } 20 + footer: { 21 + createdWith: string 22 + } 23 + graph: { 24 + title: string 25 + } 26 + recentNotes: { 27 + title: string 28 + seeRemainingMore: (variables: { remaining: number }) => string 29 + } 30 + transcludes: { 31 + transcludeOf: (variables: { targetSlug: FullSlug }) => string 32 + linkToOriginal: string 33 + } 34 + search: { 35 + title: string 36 + searchBarPlaceholder: string 37 + } 38 + tableOfContents: { 39 + title: string 40 + } 41 + } 42 + pages: { 43 + rss: { 44 + recentNotes: string 45 + lastFewNotes: (variables: { count: number }) => string 46 + } 47 + error: { 48 + title: string 49 + notFound: string 50 + } 51 + folderContent: { 52 + folder: string 53 + itemsUnderFolder: (variables: { count: number }) => string 54 + } 55 + tagContent: { 56 + tag: string 57 + tagIndex: string 58 + itemsUnderTag: (variables: { count: number }) => string 59 + showingFirst: (variables: { count: number }) => string 60 + totalTags: (variables: { count: number }) => string 61 + } 62 + } 63 + }
+65
quartz/i18n/locales/en-US.ts
··· 1 + import { Translation } from "./definition" 2 + 3 + export default { 4 + propertyDefaults: { 5 + title: "Untitled", 6 + description: "No description provided", 7 + }, 8 + components: { 9 + backlinks: { 10 + title: "Backlinks", 11 + noBacklinksFound: "No backlinks found", 12 + }, 13 + themeToggle: { 14 + lightMode: "Light mode", 15 + darkMode: "Dark mode", 16 + }, 17 + explorer: { 18 + title: "Explorer", 19 + }, 20 + footer: { 21 + createdWith: "Created with", 22 + }, 23 + graph: { 24 + title: "Graph View", 25 + }, 26 + recentNotes: { 27 + title: "Recent Notes", 28 + seeRemainingMore: ({ remaining }) => `See ${remaining} more →`, 29 + }, 30 + transcludes: { 31 + transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`, 32 + linkToOriginal: "Link to original", 33 + }, 34 + search: { 35 + title: "Search", 36 + searchBarPlaceholder: "Search for something", 37 + }, 38 + tableOfContents: { 39 + title: "Table of Contents", 40 + }, 41 + }, 42 + pages: { 43 + rss: { 44 + recentNotes: "Recent notes", 45 + lastFewNotes: ({ count }) => `Last ${count} notes`, 46 + }, 47 + error: { 48 + title: "Not Found", 49 + notFound: "Either this page is private or doesn't exist.", 50 + }, 51 + folderContent: { 52 + folder: "Folder", 53 + itemsUnderFolder: ({ count }) => 54 + count === 1 ? "1 item under this folder" : `${count} items under this folder.`, 55 + }, 56 + tagContent: { 57 + tag: "Tag", 58 + tagIndex: "Tag Index", 59 + itemsUnderTag: ({ count }) => 60 + count === 1 ? "1 item with this tag" : `${count} items with this tag.`, 61 + showingFirst: ({ count }) => `Showing first ${count} tags.`, 62 + totalTags: ({ count }) => `Found ${count} total tags.`, 63 + }, 64 + }, 65 + } as const satisfies Translation
-37
quartz/i18n/locales/en.json
··· 1 - { 2 - "404": "Either this page is private or doesn't exist.", 3 - "backlinks": { 4 - "backlinks": "Backlinks", 5 - "noBacklinksFound": "No backlinks found" 6 - }, 7 - "common": { 8 - "item": "item" 9 - }, 10 - "darkmode": { 11 - "lightMode": "Light mode" 12 - }, 13 - "folderContent": { 14 - "underThisFolder": "under this folder" 15 - }, 16 - "footer": { 17 - "createdWith": "Created with" 18 - }, 19 - "graph": { 20 - "graphView": "Graph View" 21 - }, 22 - "head": { 23 - "noDescriptionProvided": "No description provided", 24 - "untitled": "Untitled" 25 - }, 26 - "recentNotes": { 27 - "seeRemainingMore": "See {{remaining}} more" 28 - }, 29 - "search": "Search", 30 - "tableOfContent": "Table of Contents", 31 - "tagContent": { 32 - "showingFirst": "Showing first", 33 - "totalTags": "total tags", 34 - "withThisTag": "with this tag", 35 - "found": "Found" 36 - } 37 - }
+65
quartz/i18n/locales/fr-FR.ts
··· 1 + import { Translation } from "./definition" 2 + 3 + export default { 4 + propertyDefaults: { 5 + title: "Sans titre", 6 + description: "Aucune description fournie", 7 + }, 8 + components: { 9 + backlinks: { 10 + title: "Liens retour", 11 + noBacklinksFound: "Aucun lien retour trouvé", 12 + }, 13 + themeToggle: { 14 + lightMode: "Mode clair", 15 + darkMode: "Mode sombre", 16 + }, 17 + explorer: { 18 + title: "Explorateur", 19 + }, 20 + footer: { 21 + createdWith: "Créé avec", 22 + }, 23 + graph: { 24 + title: "Vue Graphique", 25 + }, 26 + recentNotes: { 27 + title: "Notes Récentes", 28 + seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`, 29 + }, 30 + transcludes: { 31 + transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`, 32 + linkToOriginal: "Lien vers l'original", 33 + }, 34 + search: { 35 + title: "Recherche", 36 + searchBarPlaceholder: "Rechercher quelque chose", 37 + }, 38 + tableOfContents: { 39 + title: "Table des Matières", 40 + }, 41 + }, 42 + pages: { 43 + rss: { 44 + recentNotes: "Notes récentes", 45 + lastFewNotes: ({ count }) => `Les dernières ${count} notes`, 46 + }, 47 + error: { 48 + title: "Pas trouvé", 49 + notFound: "Cette page est soit privée, soit elle n'existe pas.", 50 + }, 51 + folderContent: { 52 + folder: "Dossier", 53 + itemsUnderFolder: ({ count }) => 54 + count === 1 ? "1 élément sous ce dossier" : `${count} éléments sous ce dossier.`, 55 + }, 56 + tagContent: { 57 + tag: "Étiquette", 58 + tagIndex: "Index des étiquettes", 59 + itemsUnderTag: ({ count }) => 60 + count === 1 ? "1 élément avec cette étiquette" : `${count} éléments avec cette étiquette.`, 61 + showingFirst: ({ count }) => `Affichage des premières ${count} étiquettes.`, 62 + totalTags: ({ count }) => `Trouvé ${count} étiquettes au total.`, 63 + }, 64 + }, 65 + } as const satisfies Translation
-38
quartz/i18n/locales/fr.json
··· 1 - { 2 - "404": "Soit cette page est privée, soit elle n'existe pas.", 3 - "backlinks": { 4 - "backlinks": "Rétroliens", 5 - "noBacklinksFound": "Aucun rétrolien trouvé" 6 - }, 7 - "common": { 8 - "item": "fichier" 9 - }, 10 - "darkmode": { 11 - "darkmode": "Thème sombre", 12 - "lightMode": "Thème clair" 13 - }, 14 - "folderContent": { 15 - "underThisFolder": "dans ce dossier" 16 - }, 17 - "footer": { 18 - "createdWith": "Créé avec" 19 - }, 20 - "graph": { 21 - "graphView": "Vue Graphique" 22 - }, 23 - "head": { 24 - "noDescriptionProvided": "Aucune description n'a été fournie", 25 - "untitled": "Sans titre" 26 - }, 27 - "recentNotes": { 28 - "seeRemainingMore": "Voir {{remaining}} plus" 29 - }, 30 - "search": "Rechercher", 31 - "tableOfContent": "Table des Matières", 32 - "tagContent": { 33 - "showingFirst": "Afficher en premier", 34 - "totalTags": "tags totaux", 35 - "withThisTag": "avec ce tag", 36 - "found": "Trouvé" 37 - } 38 - }
+6 -4
quartz/plugins/emitters/404.tsx
··· 8 8 import { NotFound } from "../../components" 9 9 import { defaultProcessedContent } from "../vfile" 10 10 import { write } from "./helpers" 11 + import { i18n } from "../../i18n" 11 12 12 13 export const NotFoundPage: QuartzEmitterPlugin = () => { 13 14 const opts: FullPageLayout = { ··· 33 34 const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) 34 35 const path = url.pathname as FullSlug 35 36 const externalResources = pageResources(path, resources) 37 + const notFound = i18n(cfg.locale).pages.error.title 36 38 const [tree, vfile] = defaultProcessedContent({ 37 39 slug, 38 - text: "Not Found", 39 - description: "Not Found", 40 - frontmatter: { title: "Not Found", tags: [] }, 40 + text: notFound, 41 + description: notFound, 42 + frontmatter: { title: notFound, tags: [] }, 41 43 }) 42 44 const componentData: QuartzComponentProps = { 43 45 fileData: vfile.data, ··· 51 53 return [ 52 54 await write({ 53 55 ctx, 54 - content: renderPage(slug, componentData, opts, externalResources), 56 + content: renderPage(cfg, slug, componentData, opts, externalResources), 55 57 slug, 56 58 ext: ".html", 57 59 }),
+3 -2
quartz/plugins/emitters/contentIndex.ts
··· 6 6 import { QuartzEmitterPlugin } from "../types" 7 7 import { toHtml } from "hast-util-to-html" 8 8 import { write } from "./helpers" 9 + import { i18n } from "../../i18n" 9 10 10 11 export type ContentIndex = Map<FullSlug, ContentDetails> 11 12 export type ContentDetails = { ··· 38 39 const base = cfg.baseUrl ?? "" 39 40 const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url> 40 41 <loc>https://${joinSegments(base, encodeURI(slug))}</loc> 41 - <lastmod>${content.date?.toISOString()}</lastmod> 42 + ${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`} 42 43 </url>` 43 44 const urls = Array.from(idx) 44 45 .map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) ··· 78 79 <channel> 79 80 <title>${escapeHTML(cfg.pageTitle)}</title> 80 81 <link>https://${base}</link> 81 - <description>${!!limit ? `Last ${limit} notes` : "Recent notes"} on ${escapeHTML( 82 + <description>${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({ count: limit }) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML( 82 83 cfg.pageTitle, 83 84 )}</description> 84 85 <generator>Quartz -- quartz.jzhao.xyz</generator>
+1 -1
quartz/plugins/emitters/contentPage.tsx
··· 49 49 allFiles, 50 50 } 51 51 52 - const content = renderPage(slug, componentData, opts, externalResources) 52 + const content = renderPage(cfg, slug, componentData, opts, externalResources) 53 53 const fp = await write({ 54 54 ctx, 55 55 content,
+6 -2
quartz/plugins/emitters/folderPage.tsx
··· 18 18 import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" 19 19 import { FolderContent } from "../../components" 20 20 import { write } from "./helpers" 21 + import { i18n } from "../../i18n" 21 22 22 23 export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => { 23 24 const opts: FullPageLayout = { ··· 57 58 folder, 58 59 defaultProcessedContent({ 59 60 slug: joinSegments(folder, "index") as FullSlug, 60 - frontmatter: { title: `Folder: ${folder}`, tags: [] }, 61 + frontmatter: { 62 + title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`, 63 + tags: [], 64 + }, 61 65 }), 62 66 ]), 63 67 ) ··· 82 86 allFiles, 83 87 } 84 88 85 - const content = renderPage(slug, componentData, opts, externalResources) 89 + const content = renderPage(cfg, slug, componentData, opts, externalResources) 86 90 const fp = await write({ 87 91 ctx, 88 92 content,
+6 -2
quartz/plugins/emitters/tagPage.tsx
··· 15 15 import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout" 16 16 import { TagContent } from "../../components" 17 17 import { write } from "./helpers" 18 + import { i18n } from "../../i18n" 18 19 19 20 export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => { 20 21 const opts: FullPageLayout = { ··· 47 48 48 49 const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries( 49 50 [...tags].map((tag) => { 50 - const title = tag === "index" ? "Tag Index" : `Tag: #${tag}` 51 + const title = 52 + tag === "index" 53 + ? i18n(cfg.locale).pages.tagContent.tagIndex 54 + : `${i18n(cfg.locale).pages.tagContent.tag}: #${tag}` 51 55 return [ 52 56 tag, 53 57 defaultProcessedContent({ ··· 81 85 allFiles, 82 86 } 83 87 84 - const content = renderPage(slug, componentData, opts, externalResources) 88 + const content = renderPage(cfg, slug, componentData, opts, externalResources) 85 89 const fp = await write({ 86 90 ctx, 87 91 content,
+3 -2
quartz/plugins/transformers/frontmatter.ts
··· 5 5 import toml from "toml" 6 6 import { slugTag } from "../../util/path" 7 7 import { QuartzPluginData } from "../vfile" 8 + import { i18n } from "../../i18n" 8 9 9 10 export interface Options { 10 11 delims: string | string[] ··· 43 44 const opts = { ...defaultOptions, ...userOpts } 44 45 return { 45 46 name: "FrontMatter", 46 - markdownPlugins() { 47 + markdownPlugins({ cfg }) { 47 48 return [ 48 49 [remarkFrontmatter, ["yaml", "toml"]], 49 50 () => { ··· 59 60 if (data.title) { 60 61 data.title = data.title.toString() 61 62 } else if (data.title === null || data.title === undefined) { 62 - data.title = file.stem ?? "Untitled" 63 + data.title = file.stem ?? i18n(cfg.configuration.locale).propertyDefaults.title 63 64 } 64 65 65 66 const tags = coerceToArray(coalesceAliases(data, ["tags", "tag"]))
+1 -12
quartz/plugins/transformers/toc.ts
··· 3 3 import { visit } from "unist-util-visit" 4 4 import { toString } from "mdast-util-to-string" 5 5 import Slugger from "github-slugger" 6 - import { wikilinkRegex } from "./ofm" 7 6 8 7 export interface Options { 9 8 maxDepth: 1 | 2 | 3 | 4 | 5 | 6 ··· 25 24 slug: string // this is just the anchor (#some-slug), not the canonical slug 26 25 } 27 26 28 - const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g") 29 27 const slugAnchor = new Slugger() 30 28 export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = ( 31 29 userOpts, ··· 44 42 let highestDepth: number = opts.maxDepth 45 43 visit(tree, "heading", (node) => { 46 44 if (node.depth <= opts.maxDepth) { 47 - let text = toString(node) 48 - 49 - // strip link formatting from toc entries 50 - text = text.replace(wikilinkRegex, (_, rawFp, __, rawAlias) => { 51 - const fp = rawFp?.trim() ?? "" 52 - const alias = rawAlias?.slice(1).trim() 53 - return alias ?? fp 54 - }) 55 - text = text.replace(regexMdLinks, "$1") 56 - 45 + const text = toString(node) 57 46 highestDepth = Math.min(highestDepth, node.depth) 58 47 toc.push({ 59 48 depth: node.depth,
-8
quartz/util/lang.ts
··· 1 - export function pluralize(count: number, s: string): string { 2 - if (count === 1) { 3 - return `1 ${s}` 4 - } else { 5 - return `${count} ${s}s` 6 - } 7 - } 8 - 9 1 export function capitalize(s: string): string { 10 2 return s.substring(0, 1).toUpperCase() + s.substring(1) 11 3 }