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
|
||||
|
||||
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