Skip to content

Commit b8162be

Browse files
authored
feat: add i18n (#50)
* refactor: zk docs file structure * build: add content script enable content localhost by running the following script: npm run content * feat: disable i18n folder in explorer * feat: add language selector * feat: add translation page
1 parent 89344f3 commit b8162be

File tree

15 files changed

+252
-1
lines changed

15 files changed

+252
-1
lines changed

index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ declare module "*.scss" {
77
interface CustomEventMap {
88
nav: CustomEvent<{ url: FullSlug }>
99
themechange: CustomEvent<{ theme: "light" | "dark" }>
10+
langchange: CustomEvent<{ lang: string }>;
1011
}
1112

1213
declare const fetchData: Promise<ContentIndex>

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"scripts": {
1515
"quartz": "./quartz/bootstrap-cli.mjs",
1616
"docs": "npx quartz build --serve -d docs",
17+
"content": "npx quartz build --serve -d content",
1718
"check": "tsc --noEmit && npx prettier . --check",
1819
"format": "npx prettier . --write",
1920
"test": "tsx ./quartz/util/path.test.ts && tsx ./quartz/depgraph.test.ts",

quartz.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const config: QuartzConfig = {
8484
Plugin.Assets(),
8585
Plugin.Static(),
8686
Plugin.NotFoundPage(),
87+
Plugin.TranslatePage(),
8788
],
8889
},
8990
}

quartz.layout.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const defaultContentPageLayout: PageLayout = {
3030
Component.DesktopOnly(Component.Explorer()),
3131
],
3232
right: [
33+
Component.Language(),
3334
Component.Graph(),
3435
Component.DesktopOnly(Component.TableOfContents()),
3536
Component.Backlinks(),

quartz/components/ExplorerNode.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
182182
</li>
183183
) : (
184184
<li>
185-
{node.name !== "" && (
185+
{node.name !== "" && folderPath !== "i18n" && (
186186
// Node with entire folder
187187
// Render svg button + folder name, then children
188188
<div class="folder-container">

quartz/components/Language.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// @ts-ignore: this is safe, we don't want to actually make language.inline.ts a module as
2+
// modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads
3+
// see: https://v8.dev/features/modules#defer
4+
import languageScript from "./scripts/language.inline"
5+
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
6+
import languageStyle from "./styles/language.scss"
7+
8+
const languages = [
9+
{ code: 'en', name: 'English' },
10+
{ code: 'ko', name: 'Korean' },
11+
{ code: 'ja', name: 'Japanese' },
12+
{ code: 'zh', name: 'Chinese' },
13+
{ code: 'es', name: 'Spanish' },
14+
{ code: 'fr', name: 'French' }
15+
];
16+
17+
const Language: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
18+
return (
19+
<div class="language-selector">
20+
<label for="language-select">Language: </label>
21+
<select id="language-select">
22+
{languages.map(language => (
23+
<option value={language.code} key={language.code}>
24+
{language.name}
25+
</option>
26+
))}
27+
</select>
28+
</div>
29+
)
30+
}
31+
32+
Language.beforeDOMLoaded = languageScript;
33+
Language.css = languageStyle;
34+
export default (() => Language) satisfies QuartzComponentConstructor

quartz/components/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import DesktopOnly from "./DesktopOnly"
1919
import MobileOnly from "./MobileOnly"
2020
import RecentNotes from "./RecentNotes"
2121
import Breadcrumbs from "./Breadcrumbs"
22+
import Language from "./Language"
23+
import Translate from "./pages/Translate"
2224

2325
export {
2426
ArticleTitle,
@@ -42,4 +44,6 @@ export {
4244
RecentNotes,
4345
NotFound,
4446
Breadcrumbs,
47+
Language,
48+
Translate,
4549
}

quartz/components/pages/Translate.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
2+
3+
const Translate: QuartzComponent = ({ cfg }: QuartzComponentProps) => {
4+
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
5+
const baseDir = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`
6+
7+
const contributionGuideUrl = `${baseDir}Contribution-Guide`
8+
9+
return (
10+
<article class="popover-hint">
11+
<h1>Translation Not Available</h1>
12+
<p>
13+
Translation for this document is not yet available. If you are fluent in this language,
14+
please help translate it. Refer to this{" "}
15+
<a href={contributionGuideUrl}>contribution guide</a>.
16+
</p>
17+
<a href={baseDir}>Go to home</a>
18+
</article>
19+
)
20+
}
21+
22+
export default (() => Translate) satisfies QuartzComponentConstructor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
const userLang = navigator.language.startsWith("ko")
2+
? "ko"
3+
: navigator.language.startsWith("ja")
4+
? "ja"
5+
: navigator.language.startsWith("zh")
6+
? "zh"
7+
: navigator.language.startsWith("es")
8+
? "es"
9+
: navigator.language.startsWith("fr")
10+
? "fr"
11+
: "en"
12+
13+
const currentLang = localStorage.getItem("lang") ?? userLang
14+
document.documentElement.setAttribute("lang", currentLang)
15+
16+
const emitLangChangeEvent = (lang: string) => {
17+
const event = new CustomEvent("langchange", {
18+
detail: { lang },
19+
})
20+
document.dispatchEvent(event)
21+
}
22+
23+
const getCurrentPathWithoutLang = () => {
24+
const path = window.location.pathname
25+
const pathParts = path.split("/").filter((part) => part)
26+
27+
if (pathParts[0] === "i18n") {
28+
pathParts.splice(0, 2)
29+
}
30+
return pathParts.join("/")
31+
}
32+
33+
const navigateToUrl = async (url) => {
34+
try {
35+
const response = await fetch(url, { method: "HEAD" })
36+
if (response.status === 404) {
37+
window.location.href = "/translate"
38+
} else {
39+
window.location.href = url
40+
}
41+
} catch (error) {
42+
console.error("Failed to check URL status:", error)
43+
window.location.href = "/translate"
44+
}
45+
}
46+
47+
document.addEventListener("nav", () => {
48+
const switchLang = async (e: Event) => {
49+
const newLang = (e.target as HTMLSelectElement)?.value
50+
document.documentElement.setAttribute("lang", newLang)
51+
localStorage.setItem("lang", newLang)
52+
emitLangChangeEvent(newLang)
53+
54+
const currentPath = getCurrentPathWithoutLang()
55+
const newUrl = newLang === "en" ? `/${currentPath}` : `/i18n/${newLang}/${currentPath}`
56+
57+
await navigateToUrl(newUrl)
58+
}
59+
60+
const langSelect = document.querySelector("#language-select") as HTMLSelectElement
61+
langSelect.addEventListener("change", switchLang)
62+
window.addCleanup(() => langSelect.removeEventListener("change", switchLang))
63+
if (currentLang) {
64+
langSelect.value = currentLang
65+
}
66+
})
67+
68+
document.addEventListener("DOMContentLoaded", () => {
69+
const lang = localStorage.getItem("lang") ?? userLang
70+
document.documentElement.setAttribute("lang", lang)
71+
emitLangChangeEvent(lang)
72+
})
73+
74+
document.addEventListener("langchange", (event) => {
75+
const newLang = event.detail.lang
76+
console.log(`Language changed to: ${newLang}`)
77+
78+
// Update language selector value
79+
const langSelect = document.querySelector("#language-select") as HTMLSelectElement
80+
if (langSelect) {
81+
langSelect.value = newLang
82+
}
83+
})
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@use "../../styles/variables.scss" as *;
2+
3+
.language-selector {
4+
margin: 10px 0;
5+
display: flex;
6+
align-items: center;
7+
8+
& label {
9+
margin-right: 10px;
10+
font-family: var(--headerFont);
11+
font-size: 1rem;
12+
color: var(--dark);
13+
font-weight: $semiBoldWeight;
14+
}
15+
16+
& select {
17+
padding: 8px 12px;
18+
font-size: 1rem;
19+
color: var(--dark);
20+
background-color: var(--background);
21+
border: 1px solid var(--secondary);
22+
border-radius: 4px;
23+
cursor: pointer;
24+
transition: border-color 0.3s ease, box-shadow 0.3s ease;
25+
26+
&:hover {
27+
border-color: var(--tertiary);
28+
}
29+
30+
&:focus {
31+
outline: none;
32+
border-color: var(--tertiary);
33+
box-shadow: 0 0 0 3px rgba(var(--tertiary-rgb), 0.2);
34+
}
35+
}
36+
}

quartz/plugins/emitters/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export { Static } from "./static"
88
export { ComponentResources } from "./componentResources"
99
export { NotFoundPage } from "./404"
1010
export { CNAME } from "./cname"
11+
export { TranslatePage } from "./translate"

quartz/plugins/emitters/translate.tsx

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { QuartzEmitterPlugin } from "../types"
2+
import { QuartzComponentProps } from "../../components/types"
3+
import BodyConstructor from "../../components/Body"
4+
import { pageResources, renderPage } from "../../components/renderPage"
5+
import { FullPageLayout } from "../../cfg"
6+
import { FilePath, FullSlug } from "../../util/path"
7+
import { sharedPageComponents } from "../../../quartz.layout"
8+
import { Translate } from "../../components"
9+
import { defaultProcessedContent } from "../vfile"
10+
import { write } from "./helpers"
11+
import DepGraph from "../../depgraph"
12+
13+
export const TranslatePage: QuartzEmitterPlugin = () => {
14+
const opts: FullPageLayout = {
15+
...sharedPageComponents,
16+
pageBody: Translate(),
17+
beforeBody: [],
18+
left: [],
19+
right: [],
20+
}
21+
22+
const { head: Head, pageBody, footer: Footer } = opts
23+
const Body = BodyConstructor()
24+
25+
return {
26+
name: "TranslatePage",
27+
getQuartzComponents() {
28+
return [Head, Body, pageBody, Footer]
29+
},
30+
async getDependencyGraph(_ctx, _content, _resources) {
31+
return new DepGraph<FilePath>()
32+
},
33+
async emit(ctx, _content, resources): Promise<FilePath[]> {
34+
const cfg = ctx.cfg.configuration
35+
const slug = "translate" as FullSlug
36+
37+
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
38+
const path = url.pathname as FullSlug
39+
const externalResources = pageResources(path, resources)
40+
const title = "Translation Not Available"
41+
const [tree, vfile] = defaultProcessedContent({
42+
slug,
43+
text: title,
44+
description: title,
45+
frontmatter: { title, tags: [] },
46+
})
47+
const componentData: QuartzComponentProps = {
48+
ctx,
49+
fileData: vfile.data,
50+
externalResources,
51+
cfg,
52+
children: [],
53+
tree,
54+
allFiles: [],
55+
}
56+
57+
return [
58+
await write({
59+
ctx,
60+
content: renderPage(cfg, slug, componentData, opts, externalResources),
61+
slug,
62+
ext: ".html",
63+
}),
64+
]
65+
},
66+
}
67+
}

0 commit comments

Comments
 (0)