diff --git a/docs/advanced/making plugins.md b/docs/advanced/making plugins.md index 65209a2ca..565f5bdba 100644 --- a/docs/advanced/making plugins.md +++ b/docs/advanced/making plugins.md @@ -278,7 +278,7 @@ export const ContentPage: QuartzEmitterPlugin = () => { allFiles, } - const content = renderPage(slug, componentData, opts, externalResources) + const content = renderPage(cfg, slug, componentData, opts, externalResources) const fp = await emit({ content, slug: file.data.slug!, diff --git a/docs/configuration.md b/docs/configuration.md index 047f6ca6b..33d5a5744 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,6 +27,7 @@ This part of the configuration concerns anything that can affect the whole site. - `null`: don't use analytics; - `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or - `{ provider: 'google', tagId: }`: use Google Analytics +- `locale`: used for [[i18n]] and date formatting - `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. - 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` - 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. diff --git a/docs/features/i18n.md b/docs/features/i18n.md new file mode 100644 index 000000000..57547ddad --- /dev/null +++ b/docs/features/i18n.md @@ -0,0 +1,18 @@ +--- +title: Internationalization +--- + +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`. + +The locale field generally follows a certain format: `{language}-{REGION}` + +- `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). +- `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) + +> [!tip] Interested in contributing? +> We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v4/quartz/i18n/locales)! To contribute a translation, do the following things: +> +> 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file. +> 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above. +> 3. Fill in the translations! +> 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`. diff --git a/docs/index.md b/docs/index.md index cbf8719d1..f25b6e244 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ If you prefer instructions in a video format you can try following Nicole van de ## 🔧 Features -- [[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 +- [[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 - Hot-reload for both configuration and content - Simple JSX layouts and [[creating components|page components]] - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes diff --git a/quartz/cfg.ts b/quartz/cfg.ts index e7ae783f8..a477db057 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -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 { diff --git a/quartz/components/ArticleTitle.tsx b/quartz/components/ArticleTitle.tsx index 2484c946a..7768de6cb 100644 --- a/quartz/components/ArticleTitle.tsx +++ b/quartz/components/ArticleTitle.tsx @@ -9,6 +9,7 @@ function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) { return null } } + ArticleTitle.css = ` .article-title { margin: 2rem 0 0 0; diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx index 458e48b24..573c1c391 100644 --- a/quartz/components/Backlinks.tsx +++ b/quartz/components/Backlinks.tsx @@ -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 (
-

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

+

{i18n(cfg.locale).components.backlinks.title}

diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx index 056e684d3..62d3c2382 100644 --- a/quartz/components/Darkmode.tsx +++ b/quartz/components/Darkmode.tsx @@ -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" > - {i18n(cfg.locale, "darkmode.lightMode")} + {i18n(cfg.locale).components.themeToggle.darkMode} @@ -39,7 +39,7 @@ function Darkmode({ displayClass, cfg }: QuartzComponentProps) { style="enable-background:new 0 0 100 100" xmlSpace="preserve" > - {i18n(cfg.locale, "darkmode.lightMode")} + {i18n(cfg.locale).components.themeToggle.lightMode} diff --git a/quartz/components/Date.tsx b/quartz/components/Date.tsx index 6feac178c..26b59647c 100644 --- a/quartz/components/Date.tsx +++ b/quartz/components/Date.tsx @@ -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["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", diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index e964c5a41..f7017342e 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -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) => { jsonTree = JSON.stringify(folders) } - function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { + function Explorer({ cfg, allFiles, displayClass, fileData }: QuartzComponentProps) { constructFileTree(allFiles) return (
@@ -87,7 +87,7 @@ export default ((userOpts?: Partial) => { data-savestate={opts.useSavedState} data-tree={jsonTree} > -

{opts.title}

+

{opts.title ?? i18n(cfg.locale).components.explorer.title}

@@ -15,8 +15,8 @@ export default ((opts?: Options) => {

- {i18n(cfg.locale, "footer.createdWith")}{" "} - Quartz v{version}, © {year} + {i18n(cfg.locale).components.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 f728c5e5d..9fce9bd8f 100644 --- a/quartz/components/Graph.tsx +++ b/quartz/components/Graph.tsx @@ -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 (
    -

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

    +

    {i18n(cfg.locale).components.graph.title}

    { 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"}`) diff --git a/quartz/components/PageTitle.tsx b/quartz/components/PageTitle.tsx index fb1660a7c..d12960264 100644 --- a/quartz/components/PageTitle.tsx +++ b/quartz/components/PageTitle.tsx @@ -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 (

    diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx index 240ef98f3..f8f6de41f 100644 --- a/quartz/components/RecentNotes.tsx +++ b/quartz/components/RecentNotes.tsx @@ -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) => { const remaining = Math.max(0, pages.length - opts.limit) return (
    -

    {opts.title}

    +

    {opts.title ?? i18n(cfg.locale).components.recentNotes.title}

      {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) => { {opts.linkToMore && remaining > 0 && (

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

      )} diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx index b73ce0bfc..a07dbc4fd 100644 --- a/quartz/components/Search.tsx +++ b/quartz/components/Search.tsx @@ -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) => { function Search({ displayClass, cfg }: QuartzComponentProps) { const opts = { ...defaultOptions, ...userOpts } - + const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder return (
      -

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

      +

      {i18n(cfg.locale).components.search.title}

      ) => { id="search-bar" name="search" type="text" - aria-label="Search for something" - placeholder="Search for something" + aria-label={searchPlaceholder} + placeholder={searchPlaceholder} />
      diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx index 2e015076f..2abc74b53 100644 --- a/quartz/components/TableOfContents.tsx +++ b/quartz/components/TableOfContents.tsx @@ -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 (