feat: allow custom i18n routing (#5239)

pull/5240/head
T 3 weeks ago committed by GitHub
parent e635e9e5ea
commit eef57427f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,69 @@
import { ref } from 'vue'
import type { VitePressData } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { resolveLocaleLink } from 'client/theme-default/composables/langs'
function createData(
themeConfig: DefaultTheme.Config,
relativePath = 'guide/getting-started.md',
cleanUrls = false,
hash = '#install'
) {
return {
site: ref({
cleanUrls,
locales: {
root: { label: 'English', lang: 'en-US' },
fr: { label: 'Français', lang: 'fr-FR', link: '/fr/' }
},
themeConfig
}),
page: ref({ relativePath }),
theme: ref(themeConfig),
hash: ref(hash)
} as unknown as VitePressData<DefaultTheme.Config>
}
describe('client/theme-default/composables/langs', () => {
test('resolves corresponding links with the default router', () => {
expect(resolveLocaleLink(createData({}), 'fr', '/fr/', '/', true)).toBe(
'/fr/guide/getting-started.html#install'
)
})
test('resolves clean index links with the default router', () => {
expect(
resolveLocaleLink(
createData({}, 'en/guide/index.md', true, '#intro'),
'fr',
'/fr/',
'/en/',
true
)
).toBe('/fr/guide/#intro')
})
test('keeps locale root links when i18n routing is disabled', () => {
expect(
resolveLocaleLink(
createData({ i18nRouting: false }),
'fr',
'/fr/',
'/',
true
)
).toBe('/fr/#install')
})
test('uses custom i18n routing functions for corresponding links', () => {
const data = createData({
i18nRouting(data, hash, targetLocale) {
return `${data.site.value.locales[targetLocale].link}mapped/${data.page.value.relativePath}${hash}`
}
})
expect(resolveLocaleLink(data, 'fr', '/fr/', '/', true)).toBe(
'/fr/mapped/guide/getting-started.md#install'
)
})
})

@ -25,10 +25,28 @@ export default {
## i18nRouting
- Type: `boolean`
- Type: `boolean | ((data: VitePressData<DefaultTheme.Config>, hash: string, targetLocale: string) => string)`
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`.
Set `themeConfig.i18nRouting` to a function to customize the locale link. The function receives the current VitePress data, the current hash, and the target locale key, and returns the target link.
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
i18nRouting(data, hash, targetLocale) {
const target = data.site.value.locales[targetLocale]
const targetLink =
target.link || (targetLocale === 'root' ? '/' : `/${targetLocale}/`)
return `${targetLink}${data.page.value.relativePath.replace(/\.md$/, '')}${hash}`
}
}
})
```
## logo
- Type: `ThemeableImage`

@ -15,45 +15,13 @@ import {
createTitle,
inBrowser,
resolveSiteDataByRoute,
type PageData,
type SiteData
type SiteData,
type VitePressData
} from '../shared'
import type { Route } from './router'
export const dataSymbol: InjectionKey<VitePressData> = Symbol()
export interface VitePressData<T = any> {
/**
* Site-level metadata
*/
site: Ref<SiteData<T>>
/**
* themeConfig from .vitepress/config.js
*/
theme: Ref<T>
/**
* Page-level metadata
*/
page: Ref<PageData>
/**
* page frontmatter data
*/
frontmatter: Ref<PageData['frontmatter']>
/**
* dynamic route params
*/
params: Ref<PageData['params']>
title: Ref<string>
description: Ref<string>
lang: Ref<string>
dir: Ref<string>
localeIndex: Ref<string>
isDark: Ref<boolean>
/**
* Current location hash
*/
hash: Ref<string>
}
export type { VitePressData } from '../shared'
// site data is a singleton
export const siteDataRef: Ref<SiteData> = shallowRef(

@ -2,7 +2,7 @@
// so the user can do `import { useRoute, useData } from 'vitepress'`
// generic types
export type { VitePressData } from './app/data'
export type { VitePressData } from './shared'
export type { Route, Router } from './app/router'
// theme types

@ -1,9 +1,12 @@
import { computed } from 'vue'
import type { DefaultTheme } from 'vitepress/theme'
import type { VitePressData } from '../../app/data'
import { ensureStartingSlash } from '../support/utils'
import { useData } from './data'
export function useLangs({ correspondingLink = false } = {}) {
const { site, localeIndex, page, theme, hash } = useData()
const data = useData()
const { site, localeIndex } = data
const currentLang = computed(() => ({
label: site.value.locales[localeIndex.value]?.label,
link:
@ -17,15 +20,13 @@ export function useLangs({ correspondingLink = false } = {}) {
? []
: {
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
) + hash.value,
link: resolveLocaleLink(
data,
key,
value.link || (key === 'root' ? '/' : `/${key}/`),
currentLang.value.link,
correspondingLink
),
lang: value.lang,
dir: value.dir
}
@ -35,6 +36,30 @@ export function useLangs({ correspondingLink = false } = {}) {
return { localeLinks, currentLang }
}
export function resolveLocaleLink(
data: VitePressData<DefaultTheme.Config>,
targetLocale: string,
targetLink: string,
currentLink: string,
correspondingLink: boolean
) {
const { site, page, theme, hash } = data
const i18nRouting = theme.value.i18nRouting
if (correspondingLink && typeof i18nRouting === 'function') {
return i18nRouting(data, hash.value, targetLocale)
}
return (
normalizeLink(
targetLink,
i18nRouting !== false && correspondingLink,
page.value.relativePath.slice(currentLink.length - 1),
!site.value.cleanUrls
) + hash.value
)
}
function normalizeLink(
link: string,
addPath: boolean,

@ -16,6 +16,7 @@ export type {
PageData,
PageDataPayload,
SiteData,
VitePressData,
SSGContext,
AdditionalConfig,
AdditionalConfigDict,

@ -1,7 +1,7 @@
import type { Options as _MiniSearchOptions } from 'minisearch'
import type { DocSearchProps } from './docsearch.js'
import type { LocalSearchTranslations } from './local-search.js'
import type { Header, PageData } from './shared.js'
import type { Header, PageData, VitePressData } from './shared.js'
export namespace DefaultTheme {
export interface Config {
@ -136,11 +136,13 @@ export namespace DefaultTheme {
carbonAds?: CarbonAdsOptions
/**
* Changing locale when current url is `/foo` will redirect to `/locale/foo`.
* Changing locale when current url is `/foo` redirects to `/locale/foo`.
* Set to `false` to disable this behavior, or provide a function to
* customize the target locale link.
*
* @default true
*/
i18nRouting?: boolean
i18nRouting?: boolean | I18nRouting
/**
* Show external link icon in Markdown links.
@ -155,6 +157,12 @@ export namespace DefaultTheme {
notFound?: NotFoundOptions
}
export type I18nRouting = (
data: VitePressData<Config>,
hash: string,
targetLocale: string
) => string
// nav -----------------------------------------------------------------------
export type NavItem = NavItemComponent | NavItemWithLink | NavItemWithChildren

34
types/shared.d.ts vendored

@ -1,5 +1,6 @@
// types shared between server and client
import type { UseDarkOptions } from '@vueuse/core'
import type { Ref } from 'vue'
import type { SSRContext } from 'vue/server-renderer'
export type { DefaultTheme } from './default-theme.js'
@ -146,6 +147,39 @@ export interface SiteData<ThemeConfig = any> {
AdditionalConfigDict<ThemeConfig> | AdditionalConfigLoader<ThemeConfig>
}
export interface VitePressData<T = any> {
/**
* Site-level metadata
*/
site: Ref<SiteData<T>>
/**
* themeConfig from .vitepress/config.js
*/
theme: Ref<T>
/**
* Page-level metadata
*/
page: Ref<PageData>
/**
* page frontmatter data
*/
frontmatter: Ref<PageData['frontmatter']>
/**
* dynamic route params
*/
params: Ref<PageData['params']>
title: Ref<string>
description: Ref<string>
lang: Ref<string>
dir: Ref<string>
localeIndex: Ref<string>
isDark: Ref<boolean>
/**
* Current location hash
*/
hash: Ref<string>
}
export type HeadConfig =
[string, Record<string, string>] | [string, Record<string, string>, string]

Loading…
Cancel
Save