@ -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 @@ export interface GlobalConfiguration {
|
||||
/**
|
||||
* 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 {
|
||||
|
@ -9,6 +9,7 @@ function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
ArticleTitle.css = `
|
||||
.article-title {
|
||||
margin: 2rem 0 0 0;
|
||||
|
@ -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 @@ function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentPro
|
||||
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 @@ function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentPro
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li>{i18n(cfg.locale, "backlinks.noBacklinksFound")}</li>
|
||||
<li>{i18n(cfg.locale).components.backlinks.noBacklinksFound}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -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 @@ function Darkmode({ displayClass, cfg }: QuartzComponentProps) {
|
||||
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 @@ function Darkmode({ displayClass, cfg }: QuartzComponentProps) {
|
||||
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>
|
||||
|
@ -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 @@ export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date
|
||||
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",
|
||||
|
@ -6,10 +6,10 @@ import script from "./scripts/explorer.inline"
|
||||
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 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
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 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
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"
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
type OrderEntries = "sort" | "filter" | "map"
|
||||
|
||||
export interface Options {
|
||||
title: string
|
||||
title?: string
|
||||
folderDefaultState: "collapsed" | "open"
|
||||
folderClickBehavior: "collapse" | "link"
|
||||
useSavedState: boolean
|
||||
|
@ -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 @@ export default ((opts?: Options) => {
|
||||
<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]) => (
|
||||
|
@ -2,7 +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 { i18n } from "../i18n"
|
||||
import { classNames } from "../util/lang"
|
||||
|
||||
export interface D3Config {
|
||||
@ -59,7 +59,7 @@ export default ((opts?: GraphOptions) => {
|
||||
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
|
||||
|
@ -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"}`)
|
||||
|
@ -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")}>
|
||||
|
@ -5,11 +5,11 @@ 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 { 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 @@ interface Options {
|
||||
}
|
||||
|
||||
const defaultOptions = (cfg: GlobalConfiguration): Options => ({
|
||||
title: "Recent Notes",
|
||||
limit: 3,
|
||||
linkToMore: false,
|
||||
filter: () => true,
|
||||
@ -31,10 +30,10 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
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 @@ export default ((userOpts?: Partial<Options>) => {
|
||||
{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>
|
||||
)}
|
||||
|
@ -3,7 +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"
|
||||
import { i18n } from "../i18n"
|
||||
|
||||
export interface SearchOptions {
|
||||
enablePreview: boolean
|
||||
@ -16,11 +16,11 @@ const defaultOptions: SearchOptions = {
|
||||
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 @@ export default ((userOpts?: Partial<SearchOptions>) => {
|
||||
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>
|
||||
|
@ -5,7 +5,7 @@ import { classNames } from "../util/lang"
|
||||
|
||||
// @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 @@ function TableOfContents({ fileData, displayClass, cfg }: QuartzComponentProps)
|
||||
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 @@ function LegacyTableOfContents({ fileData, cfg }: QuartzComponentProps) {
|
||||
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) => (
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -5,9 +5,8 @@ import style from "../styles/listPage.scss"
|
||||
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 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
||||
<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>
|
||||
|
@ -4,9 +4,8 @@ import { PageList } from "../PageList"
|
||||
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 @@ function TagContent(props: QuartzComponentProps) {
|
||||
<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 @@ function TagContent(props: QuartzComponentProps) {
|
||||
{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 @@ function TagContent(props: QuartzComponentProps) {
|
||||
<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>
|
||||
|
@ -7,6 +7,8 @@ import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../ut
|
||||
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 @@ function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, Quar
|
||||
}
|
||||
|
||||
export function renderPage(
|
||||
cfg: GlobalConfiguration,
|
||||
slug: FullSlug,
|
||||
componentData: QuartzComponentProps,
|
||||
components: RenderComponents,
|
||||
@ -136,7 +139,9 @@ export function renderPage(
|
||||
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 @@ export function renderPage(
|
||||
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 @@ export function renderPage(
|
||||
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 },
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
11
quartz/i18n/index.ts
Normal file
11
quartz/i18n/index.ts
Normal file
@ -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
|
63
quartz/i18n/locales/definition.ts
Normal file
63
quartz/i18n/locales/definition.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
65
quartz/i18n/locales/en-US.ts
Normal file
65
quartz/i18n/locales/en-US.ts
Normal file
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
65
quartz/i18n/locales/fr-FR.ts
Normal file
65
quartz/i18n/locales/fr-FR.ts
Normal file
@ -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
|
@ -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é"
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import { sharedPageComponents } from "../../../quartz.layout"
|
||||
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 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
||||
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 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
|
||||
return [
|
||||
await write({
|
||||
ctx,
|
||||
content: renderPage(slug, componentData, opts, externalResources),
|
||||
content: renderPage(cfg, slug, componentData, opts, externalResources),
|
||||
slug,
|
||||
ext: ".html",
|
||||
}),
|
||||
|
@ -6,6 +6,7 @@ import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../.
|
||||
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 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
|
||||
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 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
|
||||
<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>
|
||||
|
@ -49,7 +49,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
|
||||
allFiles,
|
||||
}
|
||||
|
||||
const content = renderPage(slug, componentData, opts, externalResources)
|
||||
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
||||
const fp = await write({
|
||||
ctx,
|
||||
content,
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
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 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
|
||||
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 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
|
||||
allFiles,
|
||||
}
|
||||
|
||||
const content = renderPage(slug, componentData, opts, externalResources)
|
||||
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
||||
const fp = await write({
|
||||
ctx,
|
||||
content,
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
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 @@ export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts)
|
||||
|
||||
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 @@ export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts)
|
||||
allFiles,
|
||||
}
|
||||
|
||||
const content = renderPage(slug, componentData, opts, externalResources)
|
||||
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
||||
const fp = await write({
|
||||
ctx,
|
||||
content,
|
||||
|
@ -5,6 +5,7 @@ import yaml from "js-yaml"
|
||||
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 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
return {
|
||||
name: "FrontMatter",
|
||||
markdownPlugins() {
|
||||
markdownPlugins({ cfg }) {
|
||||
return [
|
||||
[remarkFrontmatter, ["yaml", "toml"]],
|
||||
() => {
|
||||
@ -59,7 +60,7 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined>
|
||||
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"]))
|
||||
|
@ -3,7 +3,6 @@ import { Root } from "mdast"
|
||||
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 @@ interface TocEntry {
|
||||
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 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
|
||||
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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user