diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 4a2c9e6a..e6c9a964 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -84,7 +84,8 @@ function sidebarGuide() { { text: 'What is VitePress?', link: '/guide/what-is-vitepress' }, { text: 'Getting Started', link: '/guide/getting-started' }, { text: 'Configuration', link: '/guide/configuration' }, - { text: 'Deploying', link: '/guide/deploying' } + { text: 'Deploying', link: '/guide/deploying' }, + { text: 'Internationalization', link: '/guide/i18n' } ] }, { diff --git a/docs/config/theme-configs.md b/docs/config/theme-configs.md index e3eb1d05..a92ad2c1 100644 --- a/docs/config/theme-configs.md +++ b/docs/config/theme-configs.md @@ -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. +## 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 - Type: `ThemeableImage` diff --git a/docs/guide/api.md b/docs/guide/api.md index 44fbba27..3c04271b 100644 --- a/docs/guide/api.md +++ b/docs/guide/api.md @@ -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: ```ts -interface VitePressData { - site: Ref +interface VitePressData { + site: Ref> page: Ref - theme: Ref // themeConfig from .vitepress/config.js + theme: Ref // themeConfig from .vitepress/config.js frontmatter: Ref - lang: Ref title: Ref description: Ref - localePath: Ref + lang: Ref isDark: Ref + dir: Ref + localeIndex: Ref } ``` diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md new file mode 100644 index 00000000..01f9495b --- /dev/null +++ b/docs/guide/i18n.md @@ -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 { + 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 , or . You'll need to configure your PostCSS plugin to use `:where([dir="ltr"])` and `:where([dir="rtl"])` as prefixes to prevent CSS specificity issues. diff --git a/docs/guide/theme-search.md b/docs/guide/theme-search.md index f33d3b35..26b77a29 100644 --- a/docs/guide/theme-search.md +++ b/docs/guide/theme-search.md @@ -1,3 +1,85 @@ # 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 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. diff --git a/rollup.config.ts b/rollup.config.ts index 543eebef..66b69ee0 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -2,7 +2,7 @@ import { promises as fs } from 'fs' import { builtinModules, createRequire } from 'module' import { resolve } from 'path' import { fileURLToPath } from 'url' -import { RollupOptions, defineConfig } from 'rollup' +import { type RollupOptions, defineConfig } from 'rollup' import { nodeResolve } from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' import esbuild from 'rollup-plugin-esbuild' diff --git a/src/client/app/data.ts b/src/client/app/data.ts index 0ae27a2c..d51ae001 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -15,7 +15,6 @@ import { resolveSiteDataByRoute, createTitle } from '../shared.js' -import { withBase } from './utils.js' export const dataSymbol: InjectionKey = Symbol() @@ -27,8 +26,9 @@ export interface VitePressData { title: Ref description: Ref lang: Ref - localePath: Ref isDark: Ref + dir: Ref + localeIndex: Ref } // site data is a singleton @@ -48,7 +48,7 @@ if (import.meta.hot) { // per-app data export function initData(route: Route): VitePressData { const site = computed(() => - resolveSiteDataByRoute(siteDataRef.value, route.path) + resolveSiteDataByRoute(siteDataRef.value, route.data.relativePath) ) return { @@ -57,13 +57,8 @@ export function initData(route: Route): VitePressData { page: computed(() => route.data), frontmatter: computed(() => route.data.frontmatter), lang: computed(() => site.value.lang), - localePath: computed(() => { - const { langs, lang } = site.value - const path = Object.keys(langs).find( - (langPath) => langs[langPath].lang === lang - ) - return withBase(path || '/') - }), + dir: computed(() => site.value.dir), + localeIndex: computed(() => site.value.localeIndex || 'root'), title: computed(() => { return createTitle(site.value, route.data) }), diff --git a/src/client/app/index.ts b/src/client/app/index.ts index c6453e5e..cff3a128 100644 --- a/src/client/app/index.ts +++ b/src/client/app/index.ts @@ -5,7 +5,7 @@ import { defineComponent, h, onMounted, - watch + watchEffect } from 'vue' import Theme from '@theme/index' 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 onMounted(() => { - watch( - () => site.value.lang, - (lang: string) => { - document.documentElement.lang = lang - }, - { immediate: true } - ) + watchEffect(() => { + document.documentElement.lang = site.value.lang + document.documentElement.dir = site.value.dir + }) }) if (import.meta.env.PROD) { diff --git a/src/client/app/utils.ts b/src/client/app/utils.ts index 0ce332fa..7f64ee5d 100644 --- a/src/client/app/utils.ts +++ b/src/client/app/utils.ts @@ -1,7 +1,7 @@ import { siteDataRef } from './data.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. @@ -11,7 +11,7 @@ export function joinPath(base: string, path: string): string { } export function withBase(path: string) { - return EXTERNAL_URL_RE.test(path) + return EXTERNAL_URL_RE.test(path) || path.startsWith('.') ? path : joinPath(siteDataRef.value.base, path) } diff --git a/src/client/index.ts b/src/client/index.ts index 4d56c9ea..1ed560d9 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -11,8 +11,7 @@ export type { PageData, SiteData, HeadConfig, - Header, - LocaleConfig + Header } from '../../types/shared.js' // composables diff --git a/src/client/theme-default/Layout.vue b/src/client/theme-default/Layout.vue index 6a522720..2e62d4c8 100644 --- a/src/client/theme-default/Layout.vue +++ b/src/client/theme-default/Layout.vue @@ -1,6 +1,7 @@