mirror of https://github.com/vuejs/vitepress
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
parent
471f00a68d
commit
8de2f4499d
@ -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.
|
||||||
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
|||||||
|
import { useData as useData$ } from 'vitepress'
|
||||||
|
import type { DefaultTheme } from 'vitepress/theme'
|
||||||
|
|
||||||
|
export const useData: typeof useData$<DefaultTheme.Config> = useData$
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
Loading…
Reference in new issue