Skip to content

Commit

Permalink
feat: add i18n feature (#1339)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
3 people authored Jan 17, 2023
1 parent 471f00a commit 8de2f44
Show file tree
Hide file tree
Showing 65 changed files with 784 additions and 386 deletions.
3 changes: 2 additions & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
]
},
{
Expand Down
6 changes: 6 additions & 0 deletions docs/config/theme-configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
11 changes: 6 additions & 5 deletions docs/guide/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<SiteData>
interface VitePressData<T = any> {
site: Ref<SiteData<T>>
page: Ref<PageData>
theme: Ref<any> // themeConfig from .vitepress/config.js
theme: Ref<T> // themeConfig from .vitepress/config.js
frontmatter: Ref<PageData['frontmatter']>
lang: Ref<string>
title: Ref<string>
description: Ref<string>
localePath: Ref<string>
lang: Ref<string>
isDark: Ref<boolean>
dir: Ref<string>
localeIndex: Ref<string>
}
```

Expand Down
99 changes: 99 additions & 0 deletions docs/guide/i18n.md
Original file line number Diff line number Diff line change
@@ -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.
84 changes: 83 additions & 1 deletion docs/guide/theme-search.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
15 changes: 5 additions & 10 deletions src/client/app/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
resolveSiteDataByRoute,
createTitle
} from '../shared.js'
import { withBase } from './utils.js'

export const dataSymbol: InjectionKey<VitePressData> = Symbol()

Expand All @@ -27,8 +26,9 @@ export interface VitePressData<T = any> {
title: Ref<string>
description: Ref<string>
lang: Ref<string>
localePath: Ref<string>
isDark: Ref<boolean>
dir: Ref<string>
localeIndex: Ref<string>
}

// site data is a singleton
Expand All @@ -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 {
Expand All @@ -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)
}),
Expand Down
13 changes: 5 additions & 8 deletions src/client/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
defineComponent,
h,
onMounted,
watch
watchEffect
} from 'vue'
import Theme from '@theme/index'
import { inBrowser, pathToFile } from './utils.js'
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/client/app/utils.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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)
}
Expand Down
3 changes: 1 addition & 2 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export type {
PageData,
SiteData,
HeadConfig,
Header,
LocaleConfig
Header
} from '../../types/shared.js'

// composables
Expand Down
3 changes: 2 additions & 1 deletion src/client/theme-default/Layout.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
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 VPSkipLink from './components/VPSkipLink.vue'
import VPBackdrop from './components/VPBackdrop.vue'
Expand Down
Loading

0 comments on commit 8de2f44

Please sign in to comment.