Skip to content

Commit

Permalink
feat: add i18n (#50)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Insun35 authored Aug 3, 2024
1 parent 89344f3 commit b8162be
Show file tree
Hide file tree
Showing 15 changed files with 252 additions and 1 deletion.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ declare module "*.scss" {
interface CustomEventMap {
nav: CustomEvent<{ url: FullSlug }>
themechange: CustomEvent<{ theme: "light" | "dark" }>
langchange: CustomEvent<{ lang: string }>;
}

declare const fetchData: Promise<ContentIndex>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"scripts": {
"quartz": "./quartz/bootstrap-cli.mjs",
"docs": "npx quartz build --serve -d docs",
"content": "npx quartz build --serve -d content",
"check": "tsc --noEmit && npx prettier . --check",
"format": "npx prettier . --write",
"test": "tsx ./quartz/util/path.test.ts && tsx ./quartz/depgraph.test.ts",
Expand Down
1 change: 1 addition & 0 deletions quartz.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const config: QuartzConfig = {
Plugin.Assets(),
Plugin.Static(),
Plugin.NotFoundPage(),
Plugin.TranslatePage(),
],
},
}
Expand Down
1 change: 1 addition & 0 deletions quartz.layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const defaultContentPageLayout: PageLayout = {
Component.DesktopOnly(Component.Explorer()),
],
right: [
Component.Language(),
Component.Graph(),
Component.DesktopOnly(Component.TableOfContents()),
Component.Backlinks(),
Expand Down
2 changes: 1 addition & 1 deletion quartz/components/ExplorerNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
</li>
) : (
<li>
{node.name !== "" && (
{node.name !== "" && folderPath !== "i18n" && (
// Node with entire folder
// Render svg button + folder name, then children
<div class="folder-container">
Expand Down
34 changes: 34 additions & 0 deletions quartz/components/Language.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @ts-ignore: this is safe, we don't want to actually make language.inline.ts a module as
// modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads
// see: https://v8.dev/features/modules#defer
import languageScript from "./scripts/language.inline"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import languageStyle from "./styles/language.scss"

const languages = [
{ code: 'en', name: 'English' },
{ code: 'ko', name: 'Korean' },
{ code: 'ja', name: 'Japanese' },
{ code: 'zh', name: 'Chinese' },
{ code: 'es', name: 'Spanish' },
{ code: 'fr', name: 'French' }
];

const Language: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
return (
<div class="language-selector">
<label for="language-select">Language: </label>
<select id="language-select">
{languages.map(language => (
<option value={language.code} key={language.code}>
{language.name}
</option>
))}
</select>
</div>
)
}

Language.beforeDOMLoaded = languageScript;
Language.css = languageStyle;
export default (() => Language) satisfies QuartzComponentConstructor
4 changes: 4 additions & 0 deletions quartz/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import DesktopOnly from "./DesktopOnly"
import MobileOnly from "./MobileOnly"
import RecentNotes from "./RecentNotes"
import Breadcrumbs from "./Breadcrumbs"
import Language from "./Language"
import Translate from "./pages/Translate"

export {
ArticleTitle,
Expand All @@ -42,4 +44,6 @@ export {
RecentNotes,
NotFound,
Breadcrumbs,
Language,
Translate,
}
22 changes: 22 additions & 0 deletions quartz/components/pages/Translate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"

const Translate: QuartzComponent = ({ cfg }: QuartzComponentProps) => {
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
const baseDir = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`

const contributionGuideUrl = `${baseDir}Contribution-Guide`

return (
<article class="popover-hint">
<h1>Translation Not Available</h1>
<p>
Translation for this document is not yet available. If you are fluent in this language,
please help translate it. Refer to this{" "}
<a href={contributionGuideUrl}>contribution guide</a>.
</p>
<a href={baseDir}>Go to home</a>
</article>
)
}

export default (() => Translate) satisfies QuartzComponentConstructor
83 changes: 83 additions & 0 deletions quartz/components/scripts/language.inline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const userLang = navigator.language.startsWith("ko")
? "ko"
: navigator.language.startsWith("ja")
? "ja"
: navigator.language.startsWith("zh")
? "zh"
: navigator.language.startsWith("es")
? "es"
: navigator.language.startsWith("fr")
? "fr"
: "en"

const currentLang = localStorage.getItem("lang") ?? userLang
document.documentElement.setAttribute("lang", currentLang)

const emitLangChangeEvent = (lang: string) => {
const event = new CustomEvent("langchange", {
detail: { lang },
})
document.dispatchEvent(event)
}

const getCurrentPathWithoutLang = () => {
const path = window.location.pathname
const pathParts = path.split("/").filter((part) => part)

if (pathParts[0] === "i18n") {
pathParts.splice(0, 2)
}
return pathParts.join("/")
}

const navigateToUrl = async (url) => {
try {
const response = await fetch(url, { method: "HEAD" })
if (response.status === 404) {
window.location.href = "/translate"
} else {
window.location.href = url
}
} catch (error) {
console.error("Failed to check URL status:", error)
window.location.href = "/translate"
}
}

document.addEventListener("nav", () => {
const switchLang = async (e: Event) => {
const newLang = (e.target as HTMLSelectElement)?.value
document.documentElement.setAttribute("lang", newLang)
localStorage.setItem("lang", newLang)
emitLangChangeEvent(newLang)

const currentPath = getCurrentPathWithoutLang()
const newUrl = newLang === "en" ? `/${currentPath}` : `/i18n/${newLang}/${currentPath}`

await navigateToUrl(newUrl)
}

const langSelect = document.querySelector("#language-select") as HTMLSelectElement
langSelect.addEventListener("change", switchLang)
window.addCleanup(() => langSelect.removeEventListener("change", switchLang))
if (currentLang) {
langSelect.value = currentLang
}
})

document.addEventListener("DOMContentLoaded", () => {
const lang = localStorage.getItem("lang") ?? userLang
document.documentElement.setAttribute("lang", lang)
emitLangChangeEvent(lang)
})

document.addEventListener("langchange", (event) => {
const newLang = event.detail.lang
console.log(`Language changed to: ${newLang}`)

// Update language selector value
const langSelect = document.querySelector("#language-select") as HTMLSelectElement
if (langSelect) {
langSelect.value = newLang
}
})
36 changes: 36 additions & 0 deletions quartz/components/styles/language.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@use "../../styles/variables.scss" as *;

.language-selector {
margin: 10px 0;
display: flex;
align-items: center;

& label {
margin-right: 10px;
font-family: var(--headerFont);
font-size: 1rem;
color: var(--dark);
font-weight: $semiBoldWeight;
}

& select {
padding: 8px 12px;
font-size: 1rem;
color: var(--dark);
background-color: var(--background);
border: 1px solid var(--secondary);
border-radius: 4px;
cursor: pointer;
transition: border-color 0.3s ease, box-shadow 0.3s ease;

&:hover {
border-color: var(--tertiary);
}

&:focus {
outline: none;
border-color: var(--tertiary);
box-shadow: 0 0 0 3px rgba(var(--tertiary-rgb), 0.2);
}
}
}
1 change: 1 addition & 0 deletions quartz/plugins/emitters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { Static } from "./static"
export { ComponentResources } from "./componentResources"
export { NotFoundPage } from "./404"
export { CNAME } from "./cname"
export { TranslatePage } from "./translate"
67 changes: 67 additions & 0 deletions quartz/plugins/emitters/translate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { QuartzEmitterPlugin } from "../types"
import { QuartzComponentProps } from "../../components/types"
import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage"
import { FullPageLayout } from "../../cfg"
import { FilePath, FullSlug } from "../../util/path"
import { sharedPageComponents } from "../../../quartz.layout"
import { Translate } from "../../components"
import { defaultProcessedContent } from "../vfile"
import { write } from "./helpers"
import DepGraph from "../../depgraph"

export const TranslatePage: QuartzEmitterPlugin = () => {
const opts: FullPageLayout = {
...sharedPageComponents,
pageBody: Translate(),
beforeBody: [],
left: [],
right: [],
}

const { head: Head, pageBody, footer: Footer } = opts
const Body = BodyConstructor()

return {
name: "TranslatePage",
getQuartzComponents() {
return [Head, Body, pageBody, Footer]
},
async getDependencyGraph(_ctx, _content, _resources) {
return new DepGraph<FilePath>()
},
async emit(ctx, _content, resources): Promise<FilePath[]> {
const cfg = ctx.cfg.configuration
const slug = "translate" as FullSlug

const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
const path = url.pathname as FullSlug
const externalResources = pageResources(path, resources)
const title = "Translation Not Available"
const [tree, vfile] = defaultProcessedContent({
slug,
text: title,
description: title,
frontmatter: { title, tags: [] },
})
const componentData: QuartzComponentProps = {
ctx,
fileData: vfile.data,
externalResources,
cfg,
children: [],
tree,
allFiles: [],
}

return [
await write({
ctx,
content: renderPage(cfg, slug, componentData, opts, externalResources),
slug,
ext: ".html",
}),
]
},
}
}

0 comments on commit b8162be

Please sign in to comment.