feat: add i18n feature (#1339)

fix #291
fix #628
fix #631
fix #902
fix #955
fix #1253
fix #1381

Co-authored-by: Hiroki Okada <hirokio@tutanota.com>
Co-authored-by: Sadegh Barati <sadeghbaratiwork@gmail.com>
pull/1811/head
Divyansh Singh 2 years ago committed by GitHub
parent 471f00a68d
commit 8de2f4499d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -84,7 +84,8 @@ function sidebarGuide() {
{ text: 'What is VitePress?', link: '/guide/what-is-vitepress' }, { text: 'What is VitePress?', link: '/guide/what-is-vitepress' },
{ text: 'Getting Started', link: '/guide/getting-started' }, { text: 'Getting Started', link: '/guide/getting-started' },
{ text: 'Configuration', link: '/guide/configuration' }, { text: 'Configuration', link: '/guide/configuration' },
{ text: 'Deploying', link: '/guide/deploying' } { text: 'Deploying', link: '/guide/deploying' },
{ text: 'Internationalization', link: '/guide/i18n' }
] ]
}, },
{ {

@ -19,6 +19,12 @@ export default {
Here it describes the settings for the VitePress default theme. If you're using a custom theme created by others, these settings may not have any effect, or might behave differently. Here it describes the settings for the VitePress default theme. If you're using a custom theme created by others, these settings may not have any effect, or might behave differently.
## i18nRouting
- Type: `boolean`
Changing locale to say `zh` will change the URL from `/foo` (or `/en/foo/`) to `/zh/foo`. You can disable this behavior by setting `themeConfig.i18nRouting` to `false`.
## logo ## logo
- Type: `ThemeableImage` - Type: `ThemeableImage`

@ -11,16 +11,17 @@ Methods that start with `use*` indicates that it is a [Vue 3 Composition API](ht
Returns page-specific data. The returned object has the following type: Returns page-specific data. The returned object has the following type:
```ts ```ts
interface VitePressData { interface VitePressData<T = any> {
site: Ref<SiteData> site: Ref<SiteData<T>>
page: Ref<PageData> page: Ref<PageData>
theme: Ref<any> // themeConfig from .vitepress/config.js theme: Ref<T> // themeConfig from .vitepress/config.js
frontmatter: Ref<PageData['frontmatter']> frontmatter: Ref<PageData['frontmatter']>
lang: Ref<string>
title: Ref<string> title: Ref<string>
description: Ref<string> description: Ref<string>
localePath: Ref<string> lang: Ref<string>
isDark: Ref<boolean> isDark: Ref<boolean>
dir: Ref<string>
localeIndex: Ref<string>
} }
``` ```

@ -0,0 +1,99 @@
# Internationalization
To use the built-in i18n features, one needs to create a directory structure as follows:
```
docs/
├─ es/
│ ├─ foo.md
├─ fr/
│ ├─ foo.md
├─ foo.md
```
Then in `docs/.vitepress/config.ts`:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
// shared properties and other top-level stuff...
locales: {
root: {
label: 'English',
lang: 'en'
},
fr: {
label: 'French',
lang: 'fr', // optional, will be added as `lang` attribute on `html` tag
link: '/fr/guide' // default /fr/ -- shows on navbar translations menu, can be external
// other locale specific properties...
}
}
})
```
The following properties can be overridden for each locale (including root):
```ts
interface LocaleSpecificConfig<ThemeConfig = any> {
lang?: string
dir?: string
title?: string
titleTemplate?: string | boolean
description?: string
head?: HeadConfig[] // will be merged with existing head entries, duplicate meta tags are automatically removed
themeConfig?: ThemeConfig // will be shallow merged, common stuff can be put in top-level themeConfig entry
}
```
Refer [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) interface for details on customizing the placeholder texts of the default theme. Don't override `themeConfig.algolia` or `themeConfig.carbonAds` at locale-level. Refer [Algolia docs](./theme-search#i18n) for using multilingual search.
**Pro tip:** Config file can be stored at `docs/.vitepress/config/index.ts` too. It might help you organize stuff by creating a configuration file per locale and then merge and export them from `index.ts`.
## Separate directory for each locale
The following is a perfectly fine structure:
```
docs/
├─ en/
│ ├─ foo.md
├─ es/
│ ├─ foo.md
├─ fr/
├─ foo.md
```
However, VitePress won't redirect `/` to `/en/` by default. You'll need to configure your server for that. For example, on Netlify, you can add a `docs/public/_redirects` file like this:
```
/* /es/:splat 302 Language=es
/* /fr/:splat 302 Language=fr
/* /en/:splat 302
```
**Pro tip:** If using the above approach, you can use `nf_lang` cookie to persist user's language choice. A very basic way to do this is register a watcher inside the [setup](./theme-introduction#using-a-custom-theme) function of custom theme:
```ts
// docs/.vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
export default {
...DefaultTheme,
setup() {
const { lang } = useData()
watchEffect(() => {
if (inBrowser) {
document.cookie = `nf_lang=${lang.value}; expires=Mon, 1 Jan 2024 00:00:00 UTC; path=/`
}
})
}
}
```
## RTL Support (Experimental)
For RTL support, specify `dir: 'rtl'` in config and use some RTLCSS PostCSS plugin like <https://github.com/MohammadYounes/rtlcss>, <https://github.com/vkalinichev/postcss-rtl> or <https://github.com/elchininet/postcss-rtlcss>. You'll need to configure your PostCSS plugin to use `:where([dir="ltr"])` and `:where([dir="rtl"])` as prefixes to prevent CSS specificity issues.

@ -1,3 +1,85 @@
# Search # Search
Documentation coming soon... VitePress supports searching your docs site using [Algolia DocSearch](https://docsearch.algolia.com/docs/what-is-docsearch). Refer their getting started guide. In your `.vitepress/config.ts` you'll need to provide at least the following to make it work:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
algolia: {
appId: '...',
apiKey: '...',
indexName: '...'
}
}
})
```
If you are not eligible for DocSearch, you might wanna use some community plugins like <https://github.com/emersonbottero/vitepress-plugin-search> or explore some custom solutions on [this GitHub thread](https://github.com/vuejs/vitepress/issues/670).
## i18n
You can use a config like this to use multilingual search:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
themeConfig: {
// ...
algolia: {
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除查询条件',
resetButtonAriaLabel: '清除查询条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭',
searchByText: '搜索提供者'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
}
}
}
}
}
}
}
})
```
[These options](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) can be overridden. Refer official Algolia docs to learn more about them.

@ -2,7 +2,7 @@ import { promises as fs } from 'fs'
import { builtinModules, createRequire } from 'module' import { builtinModules, createRequire } from 'module'
import { resolve } from 'path' import { resolve } from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { RollupOptions, defineConfig } from 'rollup' import { type RollupOptions, defineConfig } from 'rollup'
import { nodeResolve } from '@rollup/plugin-node-resolve' import { nodeResolve } from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs' import commonjs from '@rollup/plugin-commonjs'
import esbuild from 'rollup-plugin-esbuild' import esbuild from 'rollup-plugin-esbuild'

@ -15,7 +15,6 @@ import {
resolveSiteDataByRoute, resolveSiteDataByRoute,
createTitle createTitle
} from '../shared.js' } from '../shared.js'
import { withBase } from './utils.js'
export const dataSymbol: InjectionKey<VitePressData> = Symbol() export const dataSymbol: InjectionKey<VitePressData> = Symbol()
@ -27,8 +26,9 @@ export interface VitePressData<T = any> {
title: Ref<string> title: Ref<string>
description: Ref<string> description: Ref<string>
lang: Ref<string> lang: Ref<string>
localePath: Ref<string>
isDark: Ref<boolean> isDark: Ref<boolean>
dir: Ref<string>
localeIndex: Ref<string>
} }
// site data is a singleton // site data is a singleton
@ -48,7 +48,7 @@ if (import.meta.hot) {
// per-app data // per-app data
export function initData(route: Route): VitePressData { export function initData(route: Route): VitePressData {
const site = computed(() => const site = computed(() =>
resolveSiteDataByRoute(siteDataRef.value, route.path) resolveSiteDataByRoute(siteDataRef.value, route.data.relativePath)
) )
return { return {
@ -57,13 +57,8 @@ export function initData(route: Route): VitePressData {
page: computed(() => route.data), page: computed(() => route.data),
frontmatter: computed(() => route.data.frontmatter), frontmatter: computed(() => route.data.frontmatter),
lang: computed(() => site.value.lang), lang: computed(() => site.value.lang),
localePath: computed(() => { dir: computed(() => site.value.dir),
const { langs, lang } = site.value localeIndex: computed(() => site.value.localeIndex || 'root'),
const path = Object.keys(langs).find(
(langPath) => langs[langPath].lang === lang
)
return withBase(path || '/')
}),
title: computed(() => { title: computed(() => {
return createTitle(site.value, route.data) return createTitle(site.value, route.data)
}), }),

@ -5,7 +5,7 @@ import {
defineComponent, defineComponent,
h, h,
onMounted, onMounted,
watch watchEffect
} from 'vue' } from 'vue'
import Theme from '@theme/index' import Theme from '@theme/index'
import { inBrowser, pathToFile } from './utils.js' import { inBrowser, pathToFile } from './utils.js'
@ -28,13 +28,10 @@ const VitePressApp = defineComponent({
// change the language on the HTML element based on the current lang // change the language on the HTML element based on the current lang
onMounted(() => { onMounted(() => {
watch( watchEffect(() => {
() => site.value.lang, document.documentElement.lang = site.value.lang
(lang: string) => { document.documentElement.dir = site.value.dir
document.documentElement.lang = lang })
},
{ immediate: true }
)
}) })
if (import.meta.env.PROD) { if (import.meta.env.PROD) {

@ -1,7 +1,7 @@
import { siteDataRef } from './data.js' import { siteDataRef } from './data.js'
import { inBrowser, EXTERNAL_URL_RE, sanitizeFileName } from '../shared.js' import { inBrowser, EXTERNAL_URL_RE, sanitizeFileName } from '../shared.js'
export { inBrowser } export { inBrowser } from '../shared.js'
/** /**
* Join two paths by resolving the slash collision. * Join two paths by resolving the slash collision.
@ -11,7 +11,7 @@ export function joinPath(base: string, path: string): string {
} }
export function withBase(path: string) { export function withBase(path: string) {
return EXTERNAL_URL_RE.test(path) return EXTERNAL_URL_RE.test(path) || path.startsWith('.')
? path ? path
: joinPath(siteDataRef.value.base, path) : joinPath(siteDataRef.value.base, path)
} }

@ -11,8 +11,7 @@ export type {
PageData, PageData,
SiteData, SiteData,
HeadConfig, HeadConfig,
Header, Header
LocaleConfig
} from '../../types/shared.js' } from '../../types/shared.js'
// composables // composables

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, provide, useSlots, watch } from 'vue' import { computed, provide, useSlots, watch } from 'vue'
import { useData, useRoute } from 'vitepress' import { useRoute } from 'vitepress'
import { useData } from './composables/data.js'
import { useSidebar, useCloseSidebarOnEscape } from './composables/sidebar.js' import { useSidebar, useCloseSidebarOnEscape } from './composables/sidebar.js'
import VPSkipLink from './components/VPSkipLink.vue' import VPSkipLink from './components/VPSkipLink.vue'
import VPBackdrop from './components/VPBackdrop.vue' import VPBackdrop from './components/VPBackdrop.vue'

@ -1,7 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from 'vitepress' import { onMounted, ref } from 'vue'
import { withBase } from 'vitepress'
import { useData } from './composables/data.js'
import { useLangs } from './composables/langs.js'
const { site } = useData() const { site } = useData()
const { localeLinks } = useLangs({ removeCurrent: false })
const root = ref('/')
onMounted(() => {
const path = window.location.pathname
.replace(site.value.base, '')
.replace(/(^.*?\/).*$/, '/$1')
if (localeLinks.value.length) {
root.value =
localeLinks.value.find(({ link }) => link.startsWith(path))?.link ||
localeLinks.value[0].link
}
})
</script> </script>
<template> <template>
@ -14,7 +30,7 @@ const { site } = useData()
</blockquote> </blockquote>
<div class="action"> <div class="action">
<a class="link" :href="site.base" aria-label="go to home"> <a class="link" :href="withBase(root)" aria-label="go to home">
Take me home Take me home
</a> </a>
</div> </div>
@ -51,7 +67,7 @@ const { site } = useData()
margin: 24px auto 18px; margin: 24px auto 18px;
width: 64px; width: 64px;
height: 1px; height: 1px;
background-color: var(--vp-c-divider) background-color: var(--vp-c-divider);
} }
.quote { .quote {
@ -74,7 +90,7 @@ const { site } = useData()
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--vp-c-brand); color: var(--vp-c-brand);
transition: border-color 0.25s, color .25s; transition: border-color 0.25s, color 0.25s;
} }
.link:hover { .link:hover {

@ -1,40 +1,47 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import docsearch from '@docsearch/js' import docsearch from '@docsearch/js'
import { onMounted } from 'vue' import { onMounted, watch } from 'vue'
import { useRouter, useRoute, useData } from 'vitepress' import { useRouter, useRoute } from 'vitepress'
import { useData } from '../composables/data.js'
const props = defineProps<{
algolia: DefaultTheme.AlgoliaSearchOptions
}>()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { theme, site } = useData() const { site, localeIndex, lang } = useData()
onMounted(() => {
initialize(theme.value.algolia)
setTimeout(poll, 16)
})
function poll() { const docsearch$ = docsearch.default ?? docsearch
// programmatically open the search box after initialize type DocSearchProps = Parameters<typeof docsearch$>[0]
const e = new Event('keydown') as any
e.key = 'k'
e.metaKey = true
window.dispatchEvent(e) onMounted(update)
watch(localeIndex, update)
setTimeout(() => { function update() {
if (!document.querySelector('.DocSearch-Modal')) { const options = {
poll() ...props.algolia,
...props.algolia.locales?.[localeIndex.value]
} }
}, 16) const rawFacetFilters = options.searchParameters?.facetFilters ?? []
const facetFilters = [
...(Array.isArray(rawFacetFilters)
? rawFacetFilters
: [rawFacetFilters]
).filter((f) => !f.startsWith('lang:')),
`lang:${lang.value}`
]
initialize({
...options,
searchParameters: {
...options.searchParameters,
facetFilters
}
})
} }
const docsearch$ = docsearch.default ?? docsearch
type DocSearchProps = Parameters<typeof docsearch$>[0]
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) { function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
// note: multi-lang search support is removed since the theme
// doesn't support multiple locales as of now.
const options = Object.assign<{}, {}, DocSearchProps>({}, userOptions, { const options = Object.assign<{}, {}, DocSearchProps>({}, userOptions, {
container: '#docsearch', container: '#docsearch',

@ -14,8 +14,10 @@ defineProps<{
.VPBackdrop { .VPBackdrop {
position: fixed; position: fixed;
top: 0; top: 0;
/*rtl:ignore*/
right: 0; right: 0;
bottom: 0; bottom: 0;
/*rtl:ignore*/
left: 0; left: 0;
z-index: var(--vp-z-index-backdrop); z-index: var(--vp-z-index-backdrop);
background: var(--vp-backdrop-bg-color); background: var(--vp-backdrop-bg-color);

@ -1,10 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { ref, watch, onMounted } from 'vue' import { ref, watch, onMounted } from 'vue'
import { useData } from 'vitepress'
import { useAside } from '../composables/aside.js' import { useAside } from '../composables/aside.js'
import { useData } from '../composables/data.js'
const { page } = useData()
const props = defineProps<{
carbonAds: DefaultTheme.CarbonAdsOptions
}>()
const carbonOptions = props.carbonAds
const { theme, page } = useData()
const carbonOptions = theme.value.carbonAds
const { isAsideEnabled } = useAside() const { isAsideEnabled } = useAside()
const container = ref() const container = ref()

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRoute, useData } from 'vitepress' import { useRoute } from 'vitepress'
import { useData } from '../composables/data.js'
import { useSidebar } from '../composables/sidebar.js' import { useSidebar } from '../composables/sidebar.js'
import VPPage from './VPPage.vue' import VPPage from './VPPage.vue'
import VPHome from './VPHome.vue' import VPHome from './VPHome.vue'

@ -1,9 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from 'vitepress' import { defineAsyncComponent } from 'vue'
import { useData } from '../composables/data.js'
import VPDocAsideOutline from './VPDocAsideOutline.vue' import VPDocAsideOutline from './VPDocAsideOutline.vue'
import VPDocAsideCarbonAds from './VPDocAsideCarbonAds.vue'
const { theme } = useData() const { theme } = useData()
const VPCarbonAds = __CARBON__
? defineAsyncComponent(() => import('./VPCarbonAds.vue'))
: () => null
</script> </script>
<template> <template>
@ -17,7 +21,7 @@ const { theme } = useData()
<div class="spacer" /> <div class="spacer" />
<slot name="aside-ads-before" /> <slot name="aside-ads-before" />
<VPDocAsideCarbonAds v-if="theme.carbonAds" /> <VPCarbonAds v-if="theme.carbonAds" :carbonAds="theme.carbonAds" />
<slot name="aside-ads-after" /> <slot name="aside-ads-after" />
<slot name="aside-bottom" /> <slot name="aside-bottom" />

@ -1,13 +0,0 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const VPCarbonAds = __CARBON__
? defineAsyncComponent(() => import('./VPCarbonAds.vue'))
: () => null
</script>
<template>
<div class="VPDocAsideCarbonAds">
<VPCarbonAds />
</div>
</template>

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { computed, inject, ref, type Ref } from 'vue' import { computed, inject, ref, type Ref } from 'vue'
import { useData } from '../composables/data.js'
import { import {
getHeaders, getHeaders,
useActiveAnchor, useActiveAnchor,
@ -43,7 +43,13 @@ function handleClick({ target: el }: Event) {
<div class="outline-marker" ref="marker" /> <div class="outline-marker" ref="marker" />
<div class="outline-title"> <div class="outline-title">
{{ theme.outlineTitle || 'On this page' }} {{
(typeof theme.outline === 'object' &&
!Array.isArray(theme.outline) &&
theme.outline.label) ||
theme.outlineTitle ||
'On this page'
}}
</div> </div>
<nav aria-labelledby="doc-outline-aria-label"> <nav aria-labelledby="doc-outline-aria-label">

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import { normalizeLink } from '../support/utils.js' import { normalizeLink } from '../support/utils.js'
import { useEditLink } from '../composables/edit-link.js' import { useEditLink } from '../composables/edit-link.js'
import { usePrevNext } from '../composables/prev-next.js' import { usePrevNext } from '../composables/prev-next.js'
@ -42,13 +42,13 @@ const showFooter = computed(() => {
<div v-if="control.prev || control.next" class="prev-next"> <div v-if="control.prev || control.next" class="prev-next">
<div class="pager"> <div class="pager">
<a v-if="control.prev" class="pager-link prev" :href="normalizeLink(control.prev.link)"> <a v-if="control.prev" class="pager-link prev" :href="normalizeLink(control.prev.link)">
<span class="desc" v-html="theme.docFooter?.prev ?? 'Previous page'"></span> <span class="desc" v-html="theme.docFooter?.prev || 'Previous page'"></span>
<span class="title" v-html="control.prev.text"></span> <span class="title" v-html="control.prev.text"></span>
</a> </a>
</div> </div>
<div class="pager" :class="{ 'has-prev': control.prev }"> <div class="pager" :class="{ 'has-prev': control.prev }">
<a v-if="control.next" class="pager-link next" :href="normalizeLink(control.next.link)"> <a v-if="control.next" class="pager-link next" :href="normalizeLink(control.next.link)">
<span class="desc" v-html="theme.docFooter?.next ?? 'Next page'"></span> <span class="desc" v-html="theme.docFooter?.next || 'Next page'"></span>
<span class="title" v-html="control.next.text"></span> <span class="title" v-html="control.next.text"></span>
</a> </a>
</div> </div>

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watchEffect, onMounted } from 'vue' import { ref, computed, watchEffect, onMounted } from 'vue'
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
const { theme, page } = useData() const { theme, page } = useData()
@ -20,7 +20,7 @@ onMounted(() => {
<template> <template>
<p class="VPLastUpdated"> <p class="VPLastUpdated">
{{ theme.lastUpdatedText ?? 'Last updated' }}: {{ theme.lastUpdatedText || 'Last updated' }}:
<time :datetime="isoDatetime">{{ datetime }}</time> <time :datetime="isoDatetime">{{ datetime }}</time>
</p> </p>
</template> </template>

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import { useSidebar } from '../composables/sidebar.js' import { useSidebar } from '../composables/sidebar.js'
const { theme } = useData() const { theme } = useData()

@ -267,6 +267,7 @@ const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean>
align-items: center; align-items: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
/*rtl:ignore*/
transform: translate(-32px, -32px); transform: translate(-32px, -32px);
} }
} }
@ -274,12 +275,14 @@ const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean>
.image-bg { .image-bg {
position: absolute; position: absolute;
top: 50%; top: 50%;
/*rtl:ignore*/
left: 50%; left: 50%;
border-radius: 50%; border-radius: 50%;
width: 192px; width: 192px;
height: 192px; height: 192px;
background-image: var(--vp-home-hero-image-background-image); background-image: var(--vp-home-hero-image-background-image);
filter: var(--vp-home-hero-image-filter); filter: var(--vp-home-hero-image-filter);
/*rtl:ignore*/
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
@ -300,8 +303,10 @@ const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean>
:deep(.image-src) { :deep(.image-src) {
position: absolute; position: absolute;
top: 50%; top: 50%;
/*rtl:ignore*/
left: 50%; left: 50%;
max-width: 192px; max-width: 192px;
/*rtl:ignore*/
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import VPFeatures from './VPFeatures.vue' import VPFeatures from './VPFeatures.vue'
const { frontmatter: fm } = useData() const { frontmatter: fm } = useData()

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import VPHero from './VPHero.vue' import VPHero from './VPHero.vue'
const { frontmatter: fm } = useData() const { frontmatter: fm } = useData()

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from '../composables/data.js'
import { useSidebar } from '../composables/sidebar.js' import { useSidebar } from '../composables/sidebar.js'
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue' import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
@ -10,6 +11,7 @@ defineEmits<{
(e: 'open-menu'): void (e: 'open-menu'): void
}>() }>()
const { theme } = useData()
const { hasSidebar } = useSidebar() const { hasSidebar } = useSidebar()
function scrollToTop() { function scrollToTop() {
@ -26,11 +28,13 @@ function scrollToTop() {
@click="$emit('open-menu')" @click="$emit('open-menu')"
> >
<VPIconAlignLeft class="menu-icon" /> <VPIconAlignLeft class="menu-icon" />
<span class="menu-text">Menu</span> <span class="menu-text">
{{ theme.sidebarMenuLabel || 'Menu' }}
</span>
</button> </button>
<a class="top-link" href="#" @click="scrollToTop"> <a class="top-link" href="#" @click="scrollToTop">
Return to top {{ theme.returnToTopLabel || 'Return to top' }}
</a> </a>
</div> </div>
</template> </template>
@ -39,6 +43,7 @@ function scrollToTop() {
.VPLocalNav { .VPLocalNav {
position: sticky; position: sticky;
top: 0; top: 0;
/*rtl:ignore*/
left: 0; left: 0;
z-index: var(--vp-z-index-local-nav); z-index: var(--vp-z-index-local-nav);
display: flex; display: flex;

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import { isActive } from '../support/utils.js' import { isActive } from '../support/utils.js'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'

@ -38,6 +38,7 @@ const classes = computed(() => ({
.VPNav { .VPNav {
position: relative; position: relative;
top: var(--vp-layout-top-height, 0px); top: var(--vp-layout-top-height, 0px);
/*rtl:ignore*/
left: 0; left: 0;
z-index: var(--vp-z-index-nav); z-index: var(--vp-z-index-nav);
width: 100%; width: 100%;

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import VPSwitchAppearance from './VPSwitchAppearance.vue' import VPSwitchAppearance from './VPSwitchAppearance.vue'
const { site } = useData() const { site } = useData()

@ -1,29 +1,38 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress' import { computed } from 'vue'
import VPFlyout from './VPFlyout.vue' import VPFlyout from './VPFlyout.vue'
import VPMenuLink from './VPMenuLink.vue' import VPMenuLink from './VPMenuLink.vue'
import VPSwitchAppearance from './VPSwitchAppearance.vue' import VPSwitchAppearance from './VPSwitchAppearance.vue'
import VPSocialLinks from './VPSocialLinks.vue' import VPSocialLinks from './VPSocialLinks.vue'
import { computed } from 'vue' import { useData } from '../composables/data.js'
import { useLangs } from '../composables/langs.js'
const { site, theme } = useData() const { site, theme } = useData()
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
const hasExtraContent = computed(() => theme.value.localeLinks || site.value.appearance || theme.value.socialLinks) const hasExtraContent = computed(
() =>
(localeLinks.value.length && currentLang.value.label) ||
site.value.appearance ||
theme.value.socialLinks
)
</script> </script>
<template> <template>
<VPFlyout v-if="hasExtraContent" class="VPNavBarExtra" label="extra navigation"> <VPFlyout v-if="hasExtraContent" class="VPNavBarExtra" label="extra navigation">
<div v-if="theme.localeLinks" class="group"> <div v-if="localeLinks.length && currentLang.label" class="group">
<p class="trans-title">{{ theme.localeLinks.text }}</p> <p class="trans-title">{{ currentLang.label }}</p>
<template v-for="locale in theme.localeLinks.items" :key="locale.link"> <template v-for="locale in localeLinks" :key="locale.link">
<VPMenuLink :item="locale" /> <VPMenuLink :item="locale" />
</template> </template>
</div> </div>
<div v-if="site.appearance" class="group"> <div v-if="site.appearance" class="group">
<div class="item appearance"> <div class="item appearance">
<p class="label">Appearance</p> <p class="label">
{{ theme.darkModeSwitchLabel || 'Appearance' }}
</p>
<div class="appearance-action"> <div class="appearance-action">
<VPSwitchAppearance /> <VPSwitchAppearance />
</div> </div>

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import VPNavBarMenuLink from './VPNavBarMenuLink.vue' import VPNavBarMenuLink from './VPNavBarMenuLink.vue'
import VPNavBarMenuGroup from './VPNavBarMenuGroup.vue' import VPNavBarMenuGroup from './VPNavBarMenuGroup.vue'

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { useData } from '../composables/data.js'
import { isActive } from '../support/utils.js' import { isActive } from '../support/utils.js'
import VPFlyout from './VPFlyout.vue' import VPFlyout from './VPFlyout.vue'

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import { isActive } from '../support/utils.js' import { isActive } from '../support/utils.js'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'

@ -1,13 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import '@docsearch/css' import '@docsearch/css'
import { defineAsyncComponent, ref, onMounted, onUnmounted } from 'vue' import {
import { useData } from 'vitepress' computed,
defineAsyncComponent,
onMounted,
onUnmounted,
ref
} from 'vue'
import { useData } from '../composables/data.js'
const VPAlgoliaSearchBox = __ALGOLIA__ const VPAlgoliaSearchBox = __ALGOLIA__
? defineAsyncComponent(() => import('./VPAlgoliaSearchBox.vue')) ? defineAsyncComponent(() => import('./VPAlgoliaSearchBox.vue'))
: () => null : () => null
const { theme } = useData() const { theme, localeIndex } = useData()
// to avoid loading the docsearch js upfront (which is more than 1/3 of the // to avoid loading the docsearch js upfront (which is more than 1/3 of the
// payload), we delay initializing it until the user has actually clicked or // payload), we delay initializing it until the user has actually clicked or
@ -15,6 +21,13 @@ const { theme } = useData()
const loaded = ref(false) const loaded = ref(false)
const metaKey = ref(`'Meta'`) const metaKey = ref(`'Meta'`)
const buttonText = computed(
() =>
theme.value.algolia?.locales?.[localeIndex.value]?.translations?.button
?.buttonText ||
theme.value.algolia?.translations?.button?.buttonText ||
'Search'
)
onMounted(() => { onMounted(() => {
if (!theme.value.algolia) { if (!theme.value.algolia) {
@ -46,13 +59,30 @@ onMounted(() => {
function load() { function load() {
if (!loaded.value) { if (!loaded.value) {
loaded.value = true loaded.value = true
setTimeout(poll, 16)
} }
} }
function poll() {
// programmatically open the search box after initialize
const e = new Event('keydown') as any
e.key = 'k'
e.metaKey = true
window.dispatchEvent(e)
setTimeout(() => {
if (!document.querySelector('.DocSearch-Modal')) {
poll()
}
}, 16)
}
</script> </script>
<template> <template>
<div v-if="theme.algolia" class="VPNavBarSearch"> <div v-if="theme.algolia" class="VPNavBarSearch">
<VPAlgoliaSearchBox v-if="loaded" /> <VPAlgoliaSearchBox v-if="loaded" :algolia="theme.algolia" />
<div v-else id="docsearch" @click="load"> <div v-else id="docsearch" @click="load">
<button <button
@ -76,7 +106,7 @@ function load() {
stroke-linejoin="round" stroke-linejoin="round"
/> />
</svg> </svg>
<span class="DocSearch-Button-Placeholder">{{ theme.algolia?.buttonText || 'Search' }}</span> <span class="DocSearch-Button-Placeholder">{{ buttonText }}</span>
</span> </span>
<span class="DocSearch-Button-Keys"> <span class="DocSearch-Button-Keys">
<kbd class="DocSearch-Button-Key"></kbd> <kbd class="DocSearch-Button-Key"></kbd>
@ -219,6 +249,8 @@ function load() {
} }
.DocSearch-Button .DocSearch-Button-Keys { .DocSearch-Button .DocSearch-Button-Keys {
/*rtl:ignore*/
direction: ltr;
display: none; display: none;
min-width: auto; min-width: auto;
} }
@ -234,9 +266,11 @@ function load() {
display: block; display: block;
margin: 2px 0 0 0; margin: 2px 0 0 0;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
/*rtl:begin:ignore*/
border-right: none; border-right: none;
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
padding-left: 6px; padding-left: 6px;
/*rtl:end:ignore*/
min-width: 0; min-width: 0;
width: auto; width: auto;
height: 22px; height: 22px;
@ -248,11 +282,13 @@ function load() {
} }
.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key { .DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
/*rtl:begin:ignore*/
border-right: 1px solid var(--vp-c-divider); border-right: 1px solid var(--vp-c-divider);
border-left: none; border-left: none;
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
padding-left: 2px; padding-left: 2px;
padding-right: 6px; padding-right: 6px;
/*rtl:end:ignore*/
} }
.DocSearch-Button .DocSearch-Button-Key:first-child { .DocSearch-Button .DocSearch-Button-Key:first-child {

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import VPSocialLinks from './VPSocialLinks.vue' import VPSocialLinks from './VPSocialLinks.vue'
const { theme } = useData() const { theme } = useData()

@ -1,17 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import { useSidebar } from '../composables/sidebar.js' import { useSidebar } from '../composables/sidebar.js'
import { useLangs } from '../composables/langs.js'
import { normalizeLink } from '../support/utils.js'
import VPImage from './VPImage.vue' import VPImage from './VPImage.vue'
const { site, theme } = useData() const { site, theme } = useData()
const { hasSidebar } = useSidebar() const { hasSidebar } = useSidebar()
const { currentLang } = useLangs()
</script> </script>
<template> <template>
<div class="VPNavBarTitle" :class="{ 'has-sidebar': hasSidebar }"> <div class="VPNavBarTitle" :class="{ 'has-sidebar': hasSidebar }">
<a class="title" :href="site.base"> <a class="title" :href="normalizeLink(currentLang.link)">
<slot name="nav-bar-title-before" /> <slot name="nav-bar-title-before" />
<VPImage class="logo" :image="theme.logo" /> <VPImage v-if="theme.logo" class="logo" :image="theme.logo" />
<template v-if="theme.siteTitle">{{ theme.siteTitle }}</template> <template v-if="theme.siteTitle">{{ theme.siteTitle }}</template>
<template v-else-if="theme.siteTitle === undefined">{{ site.title }}</template> <template v-else-if="theme.siteTitle === undefined">{{ site.title }}</template>
<slot name="nav-bar-title-after" /> <slot name="nav-bar-title-after" />

@ -1,22 +1,22 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress'
import VPIconLanguages from './icons/VPIconLanguages.vue' import VPIconLanguages from './icons/VPIconLanguages.vue'
import VPFlyout from './VPFlyout.vue' import VPFlyout from './VPFlyout.vue'
import VPMenuLink from './VPMenuLink.vue' import VPMenuLink from './VPMenuLink.vue'
import { useLangs } from '../composables/langs.js'
const { theme } = useData() const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
</script> </script>
<template> <template>
<VPFlyout <VPFlyout
v-if="theme.localeLinks" v-if="localeLinks.length && currentLang.label"
class="VPNavBarTranslations" class="VPNavBarTranslations"
:icon="VPIconLanguages" :icon="VPIconLanguages"
> >
<div class="items"> <div class="items">
<p class="title">{{ theme.localeLinks.text }}</p> <p class="title">{{ currentLang.label }}</p>
<template v-for="locale in theme.localeLinks.items" :key="locale.link"> <template v-for="locale in localeLinks" :key="locale.link">
<VPMenuLink :item="locale" /> <VPMenuLink :item="locale" />
</template> </template>
</div> </div>

@ -44,8 +44,10 @@ function unlockBodyScroll() {
.VPNavScreen { .VPNavScreen {
position: fixed; position: fixed;
top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 1px); top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 1px);
/*rtl:ignore*/
right: 0; right: 0;
bottom: 0; bottom: 0;
/*rtl:ignore*/
left: 0; left: 0;
padding: 0 32px; padding: 0 32px;
width: 100%; width: 100%;

@ -1,13 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import VPSwitchAppearance from './VPSwitchAppearance.vue' import VPSwitchAppearance from './VPSwitchAppearance.vue'
const { site } = useData() const { site, theme } = useData()
</script> </script>
<template> <template>
<div v-if="site.appearance" class="VPNavScreenAppearance"> <div v-if="site.appearance" class="VPNavScreenAppearance">
<p class="text">Appearance</p> <p class="text">
{{ theme.darkModeSwitchLabel || 'Appearance' }}
</p>
<VPSwitchAppearance /> <VPSwitchAppearance />
</div> </div>
</template> </template>

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import VPNavScreenMenuLink from './VPNavScreenMenuLink.vue' import VPNavScreenMenuLink from './VPNavScreenMenuLink.vue'
import VPNavScreenMenuGroup from './VPNavScreenMenuGroup.vue' import VPNavScreenMenuGroup from './VPNavScreenMenuGroup.vue'

@ -79,6 +79,7 @@ function toggle() {
} }
.VPNavScreenMenuGroup.open .button-icon { .VPNavScreenMenuGroup.open .button-icon {
/*rtl:ignore*/
transform: rotate(45deg); transform: rotate(45deg);
} }

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import VPSocialLinks from './VPSocialLinks.vue' import VPSocialLinks from './VPSocialLinks.vue'
const { theme } = useData() const { theme } = useData()

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useData } from 'vitepress'
import VPIconChevronDown from './icons/VPIconChevronDown.vue' import VPIconChevronDown from './icons/VPIconChevronDown.vue'
import VPIconLanguages from './icons/VPIconLanguages.vue' import VPIconLanguages from './icons/VPIconLanguages.vue'
import { useLangs } from '../composables/langs.js'
import VPLink from './VPLink.vue'
const { theme } = useData() const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
const isOpen = ref(false) const isOpen = ref(false)
function toggle() { function toggle() {
@ -14,16 +14,20 @@ function toggle() {
</script> </script>
<template> <template>
<div v-if="theme.localeLinks" class="VPNavScreenTranslations" :class="{ open: isOpen }"> <div
v-if="localeLinks.length && currentLang.label"
class="VPNavScreenTranslations"
:class="{ open: isOpen }"
>
<button class="title" @click="toggle"> <button class="title" @click="toggle">
<VPIconLanguages class="icon lang" /> <VPIconLanguages class="icon lang" />
{{ theme.localeLinks.text }} {{ currentLang.label }}
<VPIconChevronDown class="icon chevron" /> <VPIconChevronDown class="icon chevron" />
</button> </button>
<ul class="list"> <ul class="list">
<li v-for="locale in theme.localeLinks.items" :key="locale.link" class="item"> <li v-for="locale in localeLinks" :key="locale.link" class="item">
<a class="link" :href="locale.link">{{ locale.text }}</a> <VPLink class="link" :href="locale.link">{{ locale.text }}</VPLink>
</li> </li>
</ul> </ul>
</div> </div>

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { ref, watchEffect } from 'vue' import { ref, watchEffect } from 'vue'
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import { isActive } from '../support/utils.js' import { isActive } from '../support/utils.js'
import VPIconPlusSquare from './icons/VPIconPlusSquare.vue' import VPIconPlusSquare from './icons/VPIconPlusSquare.vue'
import VPIconMinusSquare from './icons/VPIconMinusSquare.vue' import VPIconMinusSquare from './icons/VPIconMinusSquare.vue'

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { type Ref, computed, inject, ref, watchEffect } from 'vue' import { type Ref, computed, inject, ref, watchEffect } from 'vue'
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import { useSidebar } from '../composables/sidebar.js' import { useSidebar } from '../composables/sidebar.js'
import { isActive } from '../support/utils.js' import { isActive } from '../support/utils.js'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'

@ -28,6 +28,7 @@
.check { .check {
position: absolute; position: absolute;
top: 1px; top: 1px;
/*rtl:ignore*/
left: 1px; left: 1px;
width: 18px; width: 18px;
height: 18px; height: 18px;

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import { useData } from 'vitepress' import { useData } from '../composables/data.js'
import { APPEARANCE_KEY } from '../../shared.js' import { APPEARANCE_KEY } from '../../shared.js'
import VPSwitch from './VPSwitch.vue' import VPSwitch from './VPSwitch.vue'
import VPIconSun from './icons/VPIconSun.vue' import VPIconSun from './icons/VPIconSun.vue'
@ -104,6 +104,7 @@ watch(checked, (newIsDark) => {
} }
.dark .VPSwitchAppearance :deep(.check) { .dark .VPSwitchAppearance :deep(.check) {
/*rtl:ignore*/
transform: translateX(18px); transform: translateX(18px);
} }
</style> </style>

@ -0,0 +1,4 @@
import { useData as useData$ } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
export const useData: typeof useData$<DefaultTheme.Config> = useData$

@ -1,11 +1,11 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useData } from 'vitepress' import { useData } from './data.js'
export function useEditLink() { export function useEditLink() {
const { theme, page } = useData() const { theme, page } = useData()
return computed(() => { return computed(() => {
const { text = 'Edit this page', pattern } = theme.value.editLink || {} const { text = 'Edit this page', pattern = '' } = theme.value.editLink || {}
const { relativePath } = page.value const { relativePath } = page.value
const url = pattern.replace(/:path/g, relativePath) const url = pattern.replace(/:path/g, relativePath)

@ -0,0 +1,50 @@
import { computed } from 'vue'
import { useData } from './data.js'
import { ensureStartingSlash } from '../support/utils.js'
export function useLangs({
removeCurrent = true,
correspondingLink = false
} = {}) {
const { site, localeIndex, page, theme } = useData()
const currentLang = computed(() => ({
label: site.value.locales[localeIndex.value]?.label,
link:
site.value.locales[localeIndex.value]?.link ||
(localeIndex.value === 'root' ? '/' : `/${localeIndex.value}/`)
}))
const localeLinks = computed(() =>
Object.entries(site.value.locales).flatMap(([key, value]) =>
removeCurrent && currentLang.value.label === value.label
? []
: {
text: value.label,
link: normalizeLink(
value.link || (key === 'root' ? '/' : `/${key}/`),
theme.value.i18nRouting !== false && correspondingLink,
page.value.relativePath.slice(currentLang.value.link.length - 1),
site.value.cleanUrls === 'disabled'
)
}
)
)
return { localeLinks, currentLang }
}
function normalizeLink(
link: string,
addPath: boolean,
path: string,
addExt: boolean
) {
return addPath
? link.replace(/\/$/, '') +
ensureStartingSlash(
path
.replace(/(^|\/)?index.md$/, '$1')
.replace(/\.md$/, addExt ? '.html' : '')
)
: link
}

@ -1,6 +1,5 @@
import type { DefaultTheme } from 'vitepress/theme' import { ref, watch } from 'vue'
import { ref, computed, watch } from 'vue' import { useRoute } from 'vitepress'
import { useData, useRoute } from 'vitepress'
export function useNav() { export function useNav() {
const isScreenOpen = ref(false) const isScreenOpen = ref(false)
@ -36,34 +35,3 @@ export function useNav() {
toggleScreen toggleScreen
} }
} }
export function useLanguageLinks() {
const { site, localePath, theme } = useData()
return computed(() => {
const langs = site.value.langs
const localePaths = Object.keys(langs)
// one language
if (localePaths.length < 2) {
return null
}
const route = useRoute()
// intentionally remove the leading slash because each locale has one
const currentPath = route.path.replace(localePath.value, '')
const candidates = localePaths.map((localePath) => ({
text: langs[localePath].label,
link: `${localePath}${currentPath}`
}))
const selectText = theme.value.selectText || 'Languages'
return {
text: selectText,
items: candidates
} as DefaultTheme.NavItemWithChildren
})
}

@ -30,8 +30,13 @@ export function getHeaders(pageOutline: DefaultTheme.Config['outline']) {
export function resolveHeaders( export function resolveHeaders(
headers: MenuItem[], headers: MenuItem[],
levelsRange: Exclude<DefaultTheme.Config['outline'], false> = 2 range?: Exclude<DefaultTheme.Config['outline'], false>
) { ) {
const levelsRange =
(typeof range === 'object' && !Array.isArray(range)
? range.level
: range) || 2
const levels: [number, number] = const levels: [number, number] =
typeof levelsRange === 'number' typeof levelsRange === 'number'
? [levelsRange, levelsRange] ? [levelsRange, levelsRange]

@ -1,5 +1,5 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useData } from 'vitepress' import { useData } from './data.js'
import { isActive } from '../support/utils.js' import { isActive } from '../support/utils.js'
import { getSidebar, getFlatSideBarLinks } from '../support/sidebar.js' import { getSidebar, getFlatSideBarLinks } from '../support/sidebar.js'

@ -6,8 +6,9 @@ import {
ref, ref,
watchEffect watchEffect
} from 'vue' } from 'vue'
import { useData, useRoute } from 'vitepress' import { useRoute } from 'vitepress'
import { useMediaQuery } from '@vueuse/core' import { useMediaQuery } from '@vueuse/core'
import { useData } from './data.js'
import { getSidebar } from '../support/sidebar.js' import { getSidebar } from '../support/sidebar.js'
export function useSidebar() { export function useSidebar() {

@ -289,6 +289,9 @@
.vp-doc [class*='language-'] pre, .vp-doc [class*='language-'] pre,
.vp-doc [class*='language-'] code { .vp-doc [class*='language-'] code {
/*rtl:ignore*/
direction: ltr;
/*rtl:ignore*/
text-align: left; text-align: left;
white-space: pre; white-space: pre;
word-spacing: normal; word-spacing: normal;
@ -389,6 +392,7 @@
} }
.vp-doc div[class*='language-'].line-numbers-mode { .vp-doc div[class*='language-'].line-numbers-mode {
/*rtl:ignore*/
padding-left: 32px; padding-left: 32px;
} }
@ -396,8 +400,10 @@
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
/*rtl:ignore*/
left: 0; left: 0;
z-index: 3; z-index: 3;
/*rtl:ignore*/
border-right: 1px solid var(--vp-code-block-divider-color); border-right: 1px solid var(--vp-code-block-divider-color);
padding-top: 16px; padding-top: 16px;
width: 32px; width: 32px;
@ -410,8 +416,11 @@
} }
.vp-doc [class*='language-'] > button.copy { .vp-doc [class*='language-'] > button.copy {
/*rtl:ignore*/
direction: ltr;
position: absolute; position: absolute;
top: 8px; top: 8px;
/*rtl:ignore*/
right: 8px; right: 8px;
z-index: 3; z-index: 3;
display: block; display: block;
@ -441,6 +450,7 @@
.vp-doc [class*='language-'] > button.copy.copied, .vp-doc [class*='language-'] > button.copy.copied,
.vp-doc [class*='language-'] > button.copy:hover.copied { .vp-doc [class*='language-'] > button.copy:hover.copied {
/*rtl:ignore*/
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
background-color: var(--vp-code-copy-code-hover-bg); background-color: var(--vp-code-copy-code-hover-bg);
background-image: var(--vp-icon-copied); background-image: var(--vp-icon-copied);
@ -449,10 +459,12 @@
.vp-doc [class*='language-'] > button.copy.copied::before, .vp-doc [class*='language-'] > button.copy.copied::before,
.vp-doc [class*='language-'] > button.copy:hover.copied::before { .vp-doc [class*='language-'] > button.copy:hover.copied::before {
position: relative; position: relative;
/*rtl:ignore*/
left: -65px; left: -65px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
/*rtl:ignore*/
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
width: 64px; width: 64px;
height: 40px; height: 40px;
@ -468,6 +480,7 @@
.vp-doc [class*='language-'] > span.lang { .vp-doc [class*='language-'] > span.lang {
position: absolute; position: absolute;
top: 6px; top: 6px;
/*rtl:ignore*/
right: 12px; right: 12px;
z-index: 2; z-index: 2;
font-size: 12px; font-size: 12px;

@ -8,7 +8,7 @@ import { ensureStartingSlash } from './utils.js'
* return empty array. * return empty array.
*/ */
export function getSidebar( export function getSidebar(
sidebar: DefaultTheme.Sidebar, sidebar: DefaultTheme.Sidebar | undefined,
path: string path: string
): DefaultTheme.SidebarGroup[] { ): DefaultTheme.SidebarGroup[] {
if (Array.isArray(sidebar)) { if (Array.isArray(sidebar)) {

@ -1,16 +1,8 @@
import { ref } from 'vue' import { withBase } from 'vitepress'
import { withBase, useData } from 'vitepress' import { useData } from '../composables/data.js'
import { EXTERNAL_URL_RE, PATHNAME_PROTOCOL_RE } from '../../shared.js' import { isExternal, PATHNAME_PROTOCOL_RE } from '../../shared.js'
export const HASH_RE = /#.*$/ export { isExternal, isActive } from '../../shared.js'
export const EXT_RE = /(index)?\.(md|html)$/
const inBrowser = typeof window !== 'undefined'
const hashRef = ref(inBrowser ? location.hash : '')
export function isExternal(path: string): boolean {
return EXTERNAL_URL_RE.test(path)
}
export function throttleAndDebounce(fn: () => void, delay: number): () => void { export function throttleAndDebounce(fn: () => void, delay: number): () => void {
let timeoutId: NodeJS.Timeout let timeoutId: NodeJS.Timeout
@ -33,42 +25,10 @@ export function throttleAndDebounce(fn: () => void, delay: number): () => void {
} }
} }
export function isActive(
currentPath: string,
matchPath?: string,
asRegex: boolean = false
): boolean {
if (matchPath === undefined) {
return false
}
currentPath = normalize(`/${currentPath}`)
if (asRegex) {
return new RegExp(matchPath).test(currentPath)
}
if (normalize(matchPath) !== currentPath) {
return false
}
const hashMatch = matchPath.match(HASH_RE)
if (hashMatch) {
return hashRef.value === hashMatch[0]
}
return true
}
export function ensureStartingSlash(path: string): string { export function ensureStartingSlash(path: string): string {
return /^\//.test(path) ? path : `/${path}` return /^\//.test(path) ? path : `/${path}`
} }
export function normalize(path: string): string {
return decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '')
}
export function normalizeLink(url: string): string { export function normalizeLink(url: string): string {
if (isExternal(url)) { if (isExternal(url)) {
return url.replace(PATHNAME_PROTOCOL_RE, '') return url.replace(PATHNAME_PROTOCOL_RE, '')
@ -80,10 +40,13 @@ export function normalizeLink(url: string): string {
const normalizedPath = const normalizedPath =
pathname.endsWith('/') || pathname.endsWith('.html') pathname.endsWith('/') || pathname.endsWith('.html')
? url ? url
: `${pathname.replace( : url.replace(
/(?:(^\.+)\/)?.*$/,
`$1${pathname.replace(
/(\.md)?$/, /(\.md)?$/,
site.value.cleanUrls === 'disabled' ? '.html' : '' site.value.cleanUrls === 'disabled' ? '.html' : ''
)}${search}${hash}` )}${search}${hash}`
)
return withBase(normalizedPath) return withBase(normalizedPath)
} }

@ -4,16 +4,17 @@ import path from 'path'
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup' import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup'
import { pathToFileURL } from 'url' import { pathToFileURL } from 'url'
import { normalizePath, transformWithEsbuild } from 'vite' import { normalizePath, transformWithEsbuild } from 'vite'
import { resolveSiteDataByRoute, type SiteConfig } from '../config' import type { SiteConfig } from '../config'
import type { SSGContext } from '../shared'
import { import {
createTitle, createTitle,
EXTERNAL_URL_RE, EXTERNAL_URL_RE,
mergeHead, mergeHead,
notFoundPageData, notFoundPageData,
resolveSiteDataByRoute,
sanitizeFileName, sanitizeFileName,
type HeadConfig, type HeadConfig,
type PageData type PageData,
type SSGContext
} from '../shared' } from '../shared'
import { slash } from '../utils/slash' import { slash } from '../utils/slash'
@ -146,7 +147,7 @@ export async function renderPage(
const html = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
<html lang="${siteData.lang}"> <html lang="${siteData.lang}" dir="${siteData.dir}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">

@ -10,38 +10,43 @@ import {
normalizePath, normalizePath,
type UserConfig as ViteConfig type UserConfig as ViteConfig
} from 'vite' } from 'vite'
import type { SSGContext } from '../../types/shared'
import { DEFAULT_THEME_PATH } from './alias' import { DEFAULT_THEME_PATH } from './alias'
import type { MarkdownOptions } from './markdown/markdown' import type { MarkdownOptions } from './markdown/markdown'
import { import {
APPEARANCE_KEY, APPEARANCE_KEY,
createLangDictionary,
type Awaitable, type Awaitable,
type CleanUrlsMode, type CleanUrlsMode,
type DefaultTheme, type DefaultTheme,
type HeadConfig, type HeadConfig,
type LocaleConfig, type LocaleConfig,
type LocaleSpecificConfig,
type PageData, type PageData,
type SiteData type SiteData,
type SSGContext
} from './shared' } from './shared'
export { resolveSiteDataByRoute } from './shared'
const debug = _debug('vitepress:config') const debug = _debug('vitepress:config')
export interface UserConfig<ThemeConfig = any> { export interface UserConfig<ThemeConfig = any>
extends LocaleSpecificConfig<ThemeConfig> {
extends?: RawConfigExports<ThemeConfig> extends?: RawConfigExports<ThemeConfig>
base?: string base?: string
lang?: string srcDir?: string
title?: string srcExclude?: string[]
titleTemplate?: string | boolean outDir?: string
description?: string cacheDir?: string
head?: HeadConfig[] shouldPreload?: (link: string, page: string) => boolean
locales?: LocaleConfig<ThemeConfig>
appearance?: boolean | 'dark' appearance?: boolean | 'dark'
themeConfig?: ThemeConfig
locales?: Record<string, LocaleConfig>
markdown?: MarkdownOptions
lastUpdated?: boolean lastUpdated?: boolean
/**
* MarkdownIt options
*/
markdown?: MarkdownOptions
/** /**
* Options to pass on to `@vitejs/plugin-vue` * Options to pass on to `@vitejs/plugin-vue`
*/ */
@ -51,12 +56,6 @@ export interface UserConfig<ThemeConfig = any> {
*/ */
vite?: ViteConfig vite?: ViteConfig
srcDir?: string
srcExclude?: string[]
outDir?: string
cacheDir?: string
shouldPreload?: (link: string, page: string) => boolean
/** /**
* Configure the scroll offset when the theme has a sticky header. * Configure the scroll offset when the theme has a sticky header.
* Can be a number or a selector element to get the offset from. * Can be a number or a selector element to get the offset from.
@ -276,7 +275,10 @@ async function resolveUserConfig(
): Promise<[UserConfig, string | undefined, string[]]> { ): Promise<[UserConfig, string | undefined, string[]]> {
// load user config // load user config
const configPath = supportedConfigExtensions const configPath = supportedConfigExtensions
.map((ext) => resolve(root, `config.${ext}`)) .flatMap((ext) => [
resolve(root, `config/index.${ext}`),
resolve(root, `config.${ext}`)
])
.find(fs.pathExistsSync) .find(fs.pathExistsSync)
let userConfig: RawConfigExports = {} let userConfig: RawConfigExports = {}
@ -351,6 +353,7 @@ export async function resolveSiteData(
return { return {
lang: userConfig.lang || 'en-US', lang: userConfig.lang || 'en-US',
dir: userConfig.dir || 'ltr',
title: userConfig.title || 'VitePress', title: userConfig.title || 'VitePress',
titleTemplate: userConfig.titleTemplate, titleTemplate: userConfig.titleTemplate,
description: userConfig.description || 'A VitePress site', description: userConfig.description || 'A VitePress site',
@ -359,7 +362,6 @@ export async function resolveSiteData(
appearance: userConfig.appearance ?? true, appearance: userConfig.appearance ?? true,
themeConfig: userConfig.themeConfig || {}, themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {}, locales: userConfig.locales || {},
langs: createLangDictionary(userConfig),
scrollOffset: userConfig.scrollOffset || 90, scrollOffset: userConfig.scrollOffset || 90,
cleanUrls: userConfig.cleanUrls || 'disabled' cleanUrls: userConfig.cleanUrls || 'disabled'
} }

@ -9,6 +9,5 @@ export type {
SiteData, SiteData,
HeadConfig, HeadConfig,
Header, Header,
LocaleConfig,
DefaultTheme DefaultTheme
} from '../../types/shared' } from '../../types/shared'

@ -24,7 +24,8 @@ import { snippetPlugin } from './plugins/snippet'
import { preWrapperPlugin } from './plugins/preWrapper' import { preWrapperPlugin } from './plugins/preWrapper'
import { linkPlugin } from './plugins/link' import { linkPlugin } from './plugins/link'
import { imagePlugin } from './plugins/image' import { imagePlugin } from './plugins/image'
import type { Header } from '../shared'
export type { Header } from '../shared'
export type ThemeOptions = export type ThemeOptions =
| IThemeRegistration | IThemeRegistration
@ -51,8 +52,6 @@ export interface MarkdownOptions extends MarkdownIt.Options {
export type MarkdownRenderer = MarkdownIt export type MarkdownRenderer = MarkdownIt
export type { Header }
export const createMarkdownRenderer = async ( export const createMarkdownRenderer = async (
srcDir: string, srcDir: string,
options: MarkdownOptions = {}, options: MarkdownOptions = {},

@ -1,9 +1,4 @@
import type { import type { HeadConfig, PageData, SiteData } from '../../types/shared.js'
HeadConfig,
LocaleConfig,
PageData,
SiteData
} from '../../types/shared.js'
export type { export type {
Awaitable, Awaitable,
@ -12,6 +7,7 @@ export type {
HeadConfig, HeadConfig,
Header, Header,
LocaleConfig, LocaleConfig,
LocaleSpecificConfig,
PageData, PageData,
PageDataPayload, PageDataPayload,
SiteData, SiteData,
@ -21,6 +17,8 @@ export type {
export const EXTERNAL_URL_RE = /^[a-z]+:/i export const EXTERNAL_URL_RE = /^[a-z]+:/i
export const PATHNAME_PROTOCOL_RE = /^pathname:\/\// export const PATHNAME_PROTOCOL_RE = /^pathname:\/\//
export const APPEARANCE_KEY = 'vitepress-theme-appearance' export const APPEARANCE_KEY = 'vitepress-theme-appearance'
export const HASH_RE = /#.*$/
export const EXT_RE = /(index)?\.(md|html)$/
export const inBrowser = typeof window !== 'undefined' export const inBrowser = typeof window !== 'undefined'
@ -33,71 +31,71 @@ export const notFoundPageData: PageData = {
lastUpdated: 0 lastUpdated: 0
} }
function findMatchRoot(route: string, roots: string[]): string | undefined { export function isActive(
// first match to the routes with the most deep level. currentPath: string,
roots.sort((a, b) => { matchPath?: string,
const levelDelta = b.split('/').length - a.split('/').length asRegex: boolean = false
if (levelDelta !== 0) { ): boolean {
return levelDelta if (matchPath === undefined) {
} else { return false
return b.length - a.length
} }
})
for (const r of roots) { currentPath = normalize(`/${currentPath}`)
if (route.startsWith(r)) return r
if (asRegex) {
return new RegExp(matchPath).test(currentPath)
} }
if (normalize(matchPath) !== currentPath) {
return false
}
const hashMatch = matchPath.match(HASH_RE)
if (hashMatch) {
return (inBrowser ? location.hash : '') === hashMatch[0]
} }
function resolveLocales<T>( return true
locales: Record<string, T>,
route: string
): T | undefined {
const localeRoot = findMatchRoot(route, Object.keys(locales))
return localeRoot ? locales[localeRoot] : undefined
} }
export function createLangDictionary(siteData: { export function normalize(path: string): string {
themeConfig?: Record<string, any> return decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '')
locales?: Record<string, LocaleConfig>
}) {
const { locales } = siteData.themeConfig || {}
const siteLocales = siteData.locales
return locales && siteLocales
? Object.keys(locales).reduce((langs, path) => {
langs[path] = {
label: locales![path].label,
lang: siteLocales[path].lang
} }
return langs
}, {} as Record<string, { lang: string; label: string }>) export function isExternal(path: string): boolean {
: {} return EXTERNAL_URL_RE.test(path)
} }
// this merges the locales data to the main data by the route /**
* this merges the locales data to the main data by the route
*/
export function resolveSiteDataByRoute( export function resolveSiteDataByRoute(
siteData: SiteData, siteData: SiteData,
route: string relativePath: string
): SiteData { ): SiteData {
route = cleanRoute(siteData, route) const localeIndex =
Object.keys(siteData.locales).find(
const localeData = resolveLocales(siteData.locales || {}, route) (key) =>
const localeThemeConfig = resolveLocales<any>( key !== 'root' &&
siteData.themeConfig.locales || {}, !isExternal(key) &&
route isActive(relativePath, `/${key}/`, true)
) ) || 'root'
// avoid object rest spread since this is going to run in the browser return Object.assign({}, siteData, {
// and spread is going to result in polyfill code localeIndex,
return Object.assign({}, siteData, localeData, { lang: siteData.locales[localeIndex]?.lang ?? siteData.lang,
themeConfig: Object.assign({}, siteData.themeConfig, localeThemeConfig, { dir: siteData.locales[localeIndex]?.dir ?? siteData.dir,
// clean the locales to reduce the bundle size title: siteData.locales[localeIndex]?.title ?? siteData.title,
locales: {} titleTemplate:
}), siteData.locales[localeIndex]?.titleTemplate ?? siteData.titleTemplate,
lang: (localeData || siteData).lang, description:
// clean the locales to reduce the bundle size siteData.locales[localeIndex]?.description ?? siteData.description,
locales: {}, head: mergeHead(siteData.head, siteData.locales[localeIndex]?.head ?? []),
langs: createLangDictionary(siteData) themeConfig: {
...siteData.themeConfig,
...siteData.locales[localeIndex]?.themeConfig
}
}) })
} }
@ -136,20 +134,6 @@ function createTitleTemplate(
return ` | ${template}` return ` | ${template}`
} }
/**
* Clean up the route by removing the `base` path if it's set in config.
*/
function cleanRoute(siteData: SiteData, route: string): string {
if (!inBrowser) {
return route
}
const base = siteData.base
const baseWithoutSuffix = base.endsWith('/') ? base.slice(0, -1) : base
return route.slice(baseWithoutSuffix.length)
}
function hasTag(head: HeadConfig[], tag: HeadConfig) { function hasTag(head: HeadConfig[], tag: HeadConfig) {
const [tagType, tagAttrs] = tag const [tagType, tagAttrs] = tag
if (tagType !== 'meta') return false if (tagType !== 'meta') return false

@ -1,3 +1,5 @@
import { DocSearchProps } from './docsearch.js'
export namespace DefaultTheme { export namespace DefaultTheme {
export interface Config { export interface Config {
/** /**
@ -18,10 +20,11 @@ export namespace DefaultTheme {
* *
* @default 2 * @default 2
*/ */
outline?: number | [number, number] | 'deep' | false outline?: Outline | Outline['level'] | false
/** /**
* Custom outline title in the aside component. * @deprecated
* Use `outline.label` instead.
* *
* @default 'On this page' * @default 'On this page'
*/ */
@ -67,10 +70,19 @@ export namespace DefaultTheme {
footer?: Footer footer?: Footer
/** /**
* Adds locale menu to the nav. This option should be used when you have * @default 'Appearance'
* your translated sites outside of the project. */
darkModeSwitchLabel?: string
/**
* @default 'Menu'
*/ */
localeLinks?: LocaleLinks sidebarMenuLabel?: string
/**
* @default 'Return to top'
*/
returnToTopLabel?: string
/** /**
* The algolia options. Leave it undefined to disable the search feature. * The algolia options. Leave it undefined to disable the search feature.
@ -81,6 +93,13 @@ export namespace DefaultTheme {
* The carbon ads options. Leave it undefined to disable the ads feature. * The carbon ads options. Leave it undefined to disable the ads feature.
*/ */
carbonAds?: CarbonAdsOptions carbonAds?: CarbonAdsOptions
/**
* Changing locale when current url is `/foo` will redirect to `/locale/foo`.
*
* @default true
*/
i18nRouting?: boolean
} }
// nav ----------------------------------------------------------------------- // nav -----------------------------------------------------------------------
@ -238,16 +257,11 @@ export namespace DefaultTheme {
sponsor?: string sponsor?: string
} }
// locales ------------------------------------------------------------------- // outline -------------------------------------------------------------------
export interface LocaleLinks {
text: string
items: LocaleLink[]
}
export interface LocaleLink { export interface Outline {
text: string level?: number | [number, number] | 'deep'
link: string label?: string
} }
// algolia ------------------------------------------------------------------- // algolia -------------------------------------------------------------------
@ -256,15 +270,8 @@ export namespace DefaultTheme {
* The Algolia search options. Partially copied from * The Algolia search options. Partially copied from
* `@docsearch/react/dist/esm/DocSearch.d.ts` * `@docsearch/react/dist/esm/DocSearch.d.ts`
*/ */
export interface AlgoliaSearchOptions { export interface AlgoliaSearchOptions extends DocSearchProps {
appId: string locales?: Record<string, Partial<DocSearchProps>>
apiKey: string
indexName: string
placeholder?: string
searchParameters?: any
disableUserPersonalization?: boolean
initialQuery?: string
buttonText?: string
} }
// carbon ads ---------------------------------------------------------------- // carbon ads ----------------------------------------------------------------

143
types/docsearch.d.ts vendored

@ -0,0 +1,143 @@
export interface DocSearchProps {
appId: string
apiKey: string
indexName: string
placeholder?: string
searchParameters?: SearchOptions
disableUserPersonalization?: boolean
initialQuery?: string
translations?: DocSearchTranslations
}
export interface SearchOptions {
query?: string
similarQuery?: string
facetFilters?: string | string[]
optionalFilters?: string | string[]
numericFilters?: string | string[]
tagFilters?: string | string[]
sumOrFiltersScores?: boolean
filters?: string
page?: number
hitsPerPage?: number
offset?: number
length?: number
attributesToHighlight?: string[]
attributesToSnippet?: string[]
attributesToRetrieve?: string[]
highlightPreTag?: string
highlightPostTag?: string
snippetEllipsisText?: string
restrictHighlightAndSnippetArrays?: boolean
facets?: string[]
maxValuesPerFacet?: number
facetingAfterDistinct?: boolean
minWordSizefor1Typo?: number
minWordSizefor2Typos?: number
allowTyposOnNumericTokens?: boolean
disableTypoToleranceOnAttributes?: string[]
queryType?: 'prefixLast' | 'prefixAll' | 'prefixNone'
removeWordsIfNoResults?: 'none' | 'lastWords' | 'firstWords' | 'allOptional'
advancedSyntax?: boolean
advancedSyntaxFeatures?: ('exactPhrase' | 'excludeWords')[]
optionalWords?: string | string[]
disableExactOnAttributes?: string[]
exactOnSingleWordQuery?: 'attribute' | 'none' | 'word'
alternativesAsExact?: (
| 'ignorePlurals'
| 'singleWordSynonym'
| 'multiWordsSynonym'
)[]
enableRules?: boolean
ruleContexts?: string[]
distinct?: boolean | number
analytics?: boolean
analyticsTags?: string[]
synonyms?: boolean
replaceSynonymsInHighlight?: boolean
minProximity?: number
responseFields?: string[]
maxFacetHits?: number
percentileComputation?: boolean
clickAnalytics?: boolean
personalizationImpact?: number
enablePersonalization?: boolean
restrictSearchableAttributes?: string[]
sortFacetValuesBy?: 'count' | 'alpha'
typoTolerance?: boolean | 'min' | 'strict'
aroundLatLng?: string
aroundLatLngViaIP?: boolean
aroundRadius?: number | 'all'
aroundPrecision?: number | { from: number; value: number }[]
minimumAroundRadius?: number
insideBoundingBox?: number[][]
insidePolygon?: number[][]
ignorePlurals?: boolean | string[]
removeStopWords?: boolean | string[]
naturalLanguages?: string[]
getRankingInfo?: boolean
userToken?: string
enableABTest?: boolean
decompoundQuery?: boolean
relevancyStrictness?: number
}
export interface DocSearchTranslations {
button?: ButtonTranslations
modal?: ModalTranslations
}
export interface ButtonTranslations {
buttonText?: string
buttonAriaLabel?: string
}
export interface ModalTranslations extends ScreenStateTranslations {
searchBox?: SearchBoxTranslations
footer?: FooterTranslations
}
export interface ScreenStateTranslations {
errorScreen?: ErrorScreenTranslations
startScreen?: StartScreenTranslations
noResultsScreen?: NoResultsScreenTranslations
}
export interface SearchBoxTranslations {
resetButtonTitle?: string
resetButtonAriaLabel?: string
cancelButtonText?: string
cancelButtonAriaLabel?: string
}
export interface FooterTranslations {
selectText?: string
selectKeyAriaLabel?: string
navigateText?: string
navigateUpKeyAriaLabel?: string
navigateDownKeyAriaLabel?: string
closeText?: string
closeKeyAriaLabel?: string
searchByText?: string
}
export interface ErrorScreenTranslations {
titleText?: string
helpText?: string
}
export interface StartScreenTranslations {
recentSearchesTitle?: string
noRecentSearchesText?: string
saveRecentSearchButtonTitle?: string
removeRecentSearchButtonTitle?: string
favoriteSearchesTitle?: string
removeFavoriteSearchButtonTitle?: string
}
export interface NoResultsScreenTranslations {
noResultsText?: string
suggestedQueryText?: string
reportMissingResultsText?: string
reportMissingResultsLinkText?: string
}

57
types/shared.d.ts vendored

@ -51,14 +51,8 @@ export type CleanUrlsMode =
export interface SiteData<ThemeConfig = any> { export interface SiteData<ThemeConfig = any> {
base: string base: string
cleanUrls?: CleanUrlsMode cleanUrls?: CleanUrlsMode
/**
* Language of the site as it should be set on the `html` element.
*
* @example `en-US`, `zh-CN`
*/
lang: string lang: string
dir: string
title: string title: string
titleTemplate?: string | boolean titleTemplate?: string | boolean
description: string description: string
@ -66,44 +60,14 @@ export interface SiteData<ThemeConfig = any> {
appearance: boolean | 'dark' appearance: boolean | 'dark'
themeConfig: ThemeConfig themeConfig: ThemeConfig
scrollOffset: number | string scrollOffset: number | string
locales: Record<string, LocaleConfig> locales: LocaleConfig<ThemeConfig>
localeIndex?: string
/**
* Available locales for the site when it has defined `locales` in its
* `themeConfig`. This object is otherwise empty. Keys are paths like `/` or
* `/zh/`.
*/
langs: Record<
string,
{
/**
* Lang attribute as set on the `<html>` element.
* @example `en-US`, `zh-CN`
*/
lang: string
/**
* Label to display in the language menu.
* @example `English`, `简体中文`
*/
label: string
}
>
} }
export type HeadConfig = export type HeadConfig =
| [string, Record<string, string>] | [string, Record<string, string>]
| [string, Record<string, string>, string] | [string, Record<string, string>, string]
export interface LocaleConfig {
lang: string
title?: string
titleTemplate?: string | boolean
description?: string
head?: HeadConfig[]
label?: string
selectText?: string
}
export interface PageDataPayload { export interface PageDataPayload {
path: string path: string
pageData: PageData pageData: PageData
@ -112,3 +76,18 @@ export interface PageDataPayload {
export interface SSGContext extends SSRContext { export interface SSGContext extends SSRContext {
content: string content: string
} }
export interface LocaleSpecificConfig<ThemeConfig = any> {
lang?: string
dir?: string
title?: string
titleTemplate?: string | boolean
description?: string
head?: HeadConfig[]
themeConfig?: ThemeConfig
}
export type LocaleConfig<ThemeConfig = any> = Record<
string,
LocaleSpecificConfig<ThemeConfig> & { label: string; link?: string }
>

Loading…
Cancel
Save