-
+
diff --git a/src/app/components/icons/Language.tsx b/src/app/components/icons/Language.tsx
new file mode 100644
index 00000000..b0e156b4
--- /dev/null
+++ b/src/app/components/icons/Language.tsx
@@ -0,0 +1,11 @@
+export function Language() {
+ return (
+
+ )
+}
diff --git a/src/app/hooks/useEditLink.ts b/src/app/hooks/useEditLink.ts
index 8f42306c..743baafa 100644
--- a/src/app/hooks/useEditLink.ts
+++ b/src/app/hooks/useEditLink.ts
@@ -1,14 +1,27 @@
import { useMemo } from 'react'
+import { useLocation } from 'react-router-dom'
import { useConfig } from './useConfig.js'
import { usePageData } from './usePageData.js'
export function useEditLink() {
const pageData = usePageData()
const config = useConfig()
+ const { pathname } = useLocation()
+
+ let pathKey = ''
+ if (typeof config?.title === 'object' && Object.keys(config?.title ?? {}).length > 0) {
+ let keys: string[] = []
+ keys = Object.keys(config?.title).filter((key) => pathname.startsWith(key))
+ pathKey = keys[keys.length - 1]
+ }
+
+ const configEditLink = !config.editLink?.pattern
+ ? (config?.editLink as any)?.[pathKey]
+ : config.editLink
return useMemo(() => {
- const { pattern = '', text = 'Edit page' } = config.editLink ?? {}
+ const { pattern = '', text = 'Edit page' } = configEditLink ?? {}
let url = ''
// TODO: pattern as function
@@ -16,5 +29,5 @@ export function useEditLink() {
else if (pageData.filePath) url = pattern.replace(/:path/g, pageData.filePath)
return { url, text }
- }, [config.editLink, pageData.filePath])
+ }, [configEditLink, pageData.filePath])
}
diff --git a/src/app/hooks/useLocale.ts b/src/app/hooks/useLocale.ts
new file mode 100644
index 00000000..241d3097
--- /dev/null
+++ b/src/app/hooks/useLocale.ts
@@ -0,0 +1,27 @@
+import { useLocation } from 'react-router-dom'
+import { useConfig } from './useConfig.js'
+
+type UseLocaleReturnType = { locale: string; defaultLocale: string }
+
+export function useLocale(): UseLocaleReturnType {
+ const { pathname } = useLocation()
+ const config = useConfig()
+
+ // Get all locales
+ const prefixLocales = [
+ config?.defaultLocale?.lang,
+ ...Object.keys(config?.locales ?? {}).map((i) =>
+ config?.locales ? config.locales[i]?.lang : null,
+ ),
+ ]
+
+ // Regex for removal
+ const regexString = `^\/(${prefixLocales.join('|')})`
+ const regex = new RegExp(regexString)
+ const match = pathname.match(regex)
+
+ return {
+ locale: `${match?.[1] ?? ''}`,
+ defaultLocale: config?.defaultLocale?.lang ?? '',
+ }
+}
diff --git a/src/app/hooks/useSidebar.ts b/src/app/hooks/useSidebar.ts
index 76ddfac3..eefb90ca 100644
--- a/src/app/hooks/useSidebar.ts
+++ b/src/app/hooks/useSidebar.ts
@@ -4,7 +4,11 @@ import { useLocation } from 'react-router-dom'
import type { SidebarItem } from '../../config.js'
import { useConfig } from './useConfig.js'
-type UseSidebarReturnType = { backLink?: boolean; items: SidebarItem[]; key?: string }
+type UseSidebarReturnType = {
+ backLink?: boolean
+ items: SidebarItem[]
+ key?: string
+}
export function useSidebar(): UseSidebarReturnType {
const { pathname } = useLocation()
@@ -20,7 +24,11 @@ export function useSidebar(): UseSidebarReturnType {
}, [sidebar, pathname])
if (!sidebarKey) return { items: [] }
- if (Array.isArray(sidebar[sidebarKey]))
- return { key: sidebarKey, items: sidebar[sidebarKey] } as UseSidebarReturnType
+ if (Array.isArray(sidebar[sidebarKey])) {
+ return {
+ key: sidebarKey,
+ items: sidebar[sidebarKey],
+ } as UseSidebarReturnType
+ }
return { ...sidebar[sidebarKey], key: sidebarKey } as UseSidebarReturnType
}
diff --git a/src/app/root.tsx b/src/app/root.tsx
index 3dc1073a..54400aba 100644
--- a/src/app/root.tsx
+++ b/src/app/root.tsx
@@ -47,19 +47,30 @@ export function Root(props: {
function Head({ frontmatter }: { frontmatter: Module['frontmatter'] }) {
const config = useConfig()
const ogImageUrl = useOgImageUrl()
+ const { pathname } = useLocation()
- const { baseUrl, font, iconUrl, logoUrl } = config
+ let pathKey = ''
+ if (typeof config?.title === 'object' && Object.keys(config?.title ?? {}).length > 0) {
+ let keys: string[] = []
+ keys = Object.keys(config?.title).filter((key) => pathname.startsWith(key))
+ pathKey = keys[keys.length - 1]
+ }
- const title = frontmatter?.title ?? config.title
- const description = frontmatter?.description ?? config.description
+ const { baseUrl, font, iconUrl, logoUrl } = config
- const enableTitleTemplate = config.title && !title.includes(config.title)
+ const configTitle = typeof config?.title === 'object' ? config?.title?.[pathKey] : config?.title
+ const title = frontmatter?.title ?? configTitle
+ const configDescription =
+ typeof config?.description === 'object' ? config?.description?.[pathKey] : config?.description
+ const description = frontmatter?.description ?? configDescription
+ const enableTitleTemplate = configTitle && !title.includes(configTitle)
+ config.titleTemplate = typeof config?.title === 'object' ? `%s – ${configTitle}` : `%s – ${title}`
const isLocalhost = typeof window !== 'undefined' && window.location.hostname === 'localhost'
return (
{/* Title */}
@@ -69,7 +80,7 @@ function Head({ frontmatter }: { frontmatter: Module['frontmatter'] }) {
{baseUrl && import.meta.env.PROD && !isLocalhost && }
{/* Description */}
- {description !== 'undefined' && }
+ {configDescription !== 'undefined' && }
{/* Icons */}
{iconUrl && typeof iconUrl === 'string' && (
@@ -89,7 +100,7 @@ function Head({ frontmatter }: { frontmatter: Module['frontmatter'] }) {
{/* Open Graph */}
-
+
{baseUrl && }
{description !== 'undefined' && }
{ogImageUrl && (
diff --git a/src/config.ts b/src/config.ts
index ba28fb39..5fa692df 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -68,17 +68,39 @@ export type Config<
/**
* General description for the documentation.
*/
- description?: string
+ description?:
+ | string
+ | {
+ [path: string]: string
+ }
/**
* Edit location for the documentation.
*/
- editLink?: Normalize
+ editLink?:
+ | Normalize
+ | {
+ [path: string]: Normalize
+ }
/**
* Base font face.
*
* @default { google: "Inter" }
*/
font?: Font
+ /**
+ * Custom footer navigation text
+ */
+ footerNav?:
+ | {
+ previous: string
+ next: string
+ }
+ | {
+ [path: string]: {
+ previous: string
+ next: string
+ }
+ }
/**
* Additional tags to include in the `` tag of the page HTML.
*/
@@ -87,6 +109,14 @@ export type Config<
* Icon URL.
*/
iconUrl?: Normalize
+ /**
+ * Default selected locale
+ */
+ defaultLocale?: Locale
+ /**
+ * Localization support for i18n
+ */
+ locales?: Locales
/**
* Logo URL.
*/
@@ -110,6 +140,31 @@ export type Config<
* @default "docs"
*/
rootDir?: string
+ search?:
+ | {
+ placeholder: string
+ navigate: string
+ select: string
+ close: string
+ reset: string
+ noResults: string
+ labelClose?: string
+ labelToggle?: string
+ labelReset?: string
+ }
+ | {
+ [path: string]: {
+ placeholder: string
+ navigate: string
+ select: string
+ close: string
+ reset: string
+ noResults: string
+ labelClose?: string
+ labelToggle?: string
+ labelReset?: string
+ }
+ }
/**
* Navigation displayed on the sidebar.
*/
@@ -131,7 +186,11 @@ export type Config<
*
* @default "Docs"
*/
- title?: string
+ title?:
+ | string
+ | {
+ [path: string]: string
+ }
/**
* Template for the page title.
*
@@ -186,7 +245,7 @@ export async function defineConfig(
}),
markdown: parseMarkdown(config.markdown ?? {}),
socials: parseSocials(config.socials ?? []),
- topNav: parseTopNav(config.topNav ?? []),
+ topNav: parseTopNav(config?.topNav ?? []),
theme: await parseTheme(config.theme ?? ({} as Theme)),
vite: parseViteConfig(config.vite, {
basePath,
@@ -300,15 +359,33 @@ function parseSocials(socials: Socials): Socials {
let id = 0
function parseTopNav(topNav: TopNav): TopNav {
- const parsedTopNav: ParsedTopNavItem[] = []
- for (const item of topNav) {
- parsedTopNav.push({
- ...item,
- id: id++,
- items: item.items ? parseTopNav(item.items) : [],
- })
+ if (Array.isArray(topNav)) {
+ const parsedTopNav: ParsedTopNavItem[] = []
+ for (const item of topNav) {
+ parsedTopNav.push({
+ ...item,
+ id: id++,
+ items: item.items ? (parseTopNav(item.items) as ParsedTopNavItem[]) : [],
+ })
+ }
+ return parsedTopNav
+ } else if (typeof topNav === 'object' && Object.keys(topNav).length > 0) {
+ const parsePathTopNav: {
+ [path: string]: ParsedTopNavItem[]
+ } = {}
+ for (const path in topNav) {
+ parsePathTopNav[path] = []
+ for (const item of topNav[path]) {
+ parsePathTopNav[path].push({
+ ...item,
+ id: id++,
+ items: item.items ? (parseTopNav(item.items) as ParsedTopNavItem[]) : [],
+ })
+ }
+ }
+ return parsePathTopNav
}
- return parsedTopNav
+ return []
}
async function parseTheme(
@@ -409,6 +486,10 @@ export type EditLink = {
* @default "Edit page"
*/
text?: string
+ /**
+ * Text used for 'Last updated:'
+ */
+ lastUpdated?: string
}
export type Font = {
@@ -420,6 +501,18 @@ export type ImageUrl = string | { light: string; dark: string }
export type IconUrl = ImageUrl
+export type Locale = {
+ label: string
+ lang: string // optional, will be added as `lang` attribute on `html` tag
+ link?: string // default /fr/ -- shows on navbar translations menu, can be external
+}
+
+export type Locales =
+ | undefined
+ | {
+ [key: string]: Locale
+ }
+
export type LogoUrl = ImageUrl
export type Markdown = RequiredBy<
@@ -445,7 +538,9 @@ export type SidebarItem = {
export type Sidebar =
| SidebarItem[]
- | { [path: string]: SidebarItem[] | { backLink?: boolean; items: SidebarItem[] } }
+ | {
+ [path: string]: SidebarItem[] | { backLink?: boolean; items: SidebarItem[] }
+ }
export type SocialType = 'discord' | 'github' | 'telegram' | 'x'
export type SocialItem = {
@@ -532,14 +627,26 @@ export type Theme<
export type TopNavItem = {
match?: string
text: string
+ path?: string
} & (
| { link: string; items?: never }
- | { link?: string; items: parsed extends true ? ParsedTopNavItem[] : TopNavItem[] }
+ | {
+ link?: string
+ items: parsed extends true ? ParsedTopNavItem[] : TopNavItem[]
+ }
)
export type ParsedTopNavItem = TopNavItem & {
id: number
}
export type TopNav = parsed extends true
- ? ParsedTopNavItem[]
- : TopNavItem[]
+ ?
+ | ParsedTopNavItem[]
+ | {
+ [path: string]: ParsedTopNavItem[]
+ }
+ :
+ | TopNavItem[]
+ | {
+ [path: string]: TopNavItem[]
+ }