feat: i18n support (#50)

pull/59/head
Anthony Fu 5 years ago committed by GitHub
parent 692a490986
commit 7802cb55c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -27,11 +27,12 @@
}, },
"homepage": "https://github.com/vuejs/vitepress/tree/master/#readme", "homepage": "https://github.com/vuejs/vitepress/tree/master/#readme",
"scripts": { "scripts": {
"dev": "run-p dev-client dev-client-copy dev-node", "dev": "run-p dev-client dev-client-copy dev-node dev-shared",
"dev-client": "tsc -w -p src/client", "dev-client": "tsc -w -p src/client",
"dev-client-copy": "node scripts/watchAndCopy", "dev-client-copy": "node scripts/watchAndCopy",
"dev-node": "tsc -w -p src/node", "dev-node": "tsc -w -p src/node",
"build": "rimraf -rf dist && tsc -p src/client && tsc -p src/node && node scripts/copy", "dev-shared": "tsc -w -p src/shared",
"build": "rimraf -rf dist && tsc -p src/client && tsc -p src/node && tsc -p src/shared && node scripts/copy",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"prepublishOnly": "yarn build && yarn changelog", "prepublishOnly": "yarn build && yarn changelog",
"postpublish": "git add CHANGELOG.md && git commit -m 'chore: changelog [ci skip]'" "postpublish": "git add CHANGELOG.md && git commit -m 'chore: changelog [ci skip]'"

@ -1,8 +1,9 @@
<template> <template>
<div class="debug" :class="{ open }" @click="open = !open"> <div class="debug" :class="{ open }" @click="open = !open">
<pre>debug</pre> <pre>debug</pre>
<pre>$site {{ $site }}</pre>
<pre>$page {{ $page }}</pre> <pre>$page {{ $page }}</pre>
<pre>$siteByRoute {{ $siteByRoute }}</pre>
<pre>$site {{ $site }}</pre>
</div> </div>
</template> </template>
@ -27,8 +28,8 @@ export default {
cursor: pointer; cursor: pointer;
bottom: 0; bottom: 0;
right: 0; right: 0;
width: 50px; width: 80px;
height: 20px; height: 30px;
padding: 5px; padding: 5px;
overflow: hidden; overflow: hidden;
color: #eeeeee; color: #eeeeee;
@ -40,7 +41,7 @@ export default {
width: 500px; width: 500px;
height: 100%; height: 100%;
margin-top: 0; margin-top: 0;
padding: 5px 20px; padding: 0 0;
overflow: scroll; overflow: scroll;
} }
@ -48,5 +49,7 @@ export default {
font-family: Hack, monospace; font-family: Hack, monospace;
font-size: 13px; font-size: 13px;
margin: 0; margin: 0;
padding: 5px 10px;
border-bottom: 1px solid #eee;
} }
</style> </style>

@ -1,9 +1,11 @@
import { watchEffect } from 'vue' import { watchEffect, Ref } from 'vue'
import { siteDataRef } from './siteData'
import { PageDataRef } from './pageData' import { PageDataRef } from './pageData'
import { HeadConfig } from '../../../../types/shared' import { HeadConfig, SiteData } from '../../../../types/shared'
export function useUpdateHead(pageDataRef: PageDataRef) { export function useUpdateHead(
pageDataRef: PageDataRef,
siteDataByRouteRef: Ref<SiteData>
) {
const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta')) const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta'))
let isFirstUpdate = true let isFirstUpdate = true
@ -27,7 +29,7 @@ export function useUpdateHead(pageDataRef: PageDataRef) {
watchEffect(() => { watchEffect(() => {
const pageData = pageDataRef.value const pageData = pageDataRef.value
const siteData = siteDataRef.value const siteData = siteDataByRouteRef.value
const pageTitle = pageData && pageData.title const pageTitle = pageData && pageData.title
document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title
updateHeadTags([ updateHeadTags([

@ -0,0 +1,10 @@
import { computed } from 'vue'
import { resolveSiteDataByRoute } from '/@shared/config'
import { siteDataRef } from './siteData'
import { useRoute } from '../router'
export function useSiteDataByRoute(route = useRoute()) {
return computed(() => {
return resolveSiteDataByRoute(siteDataRef.value, route.path)
})
}

@ -7,6 +7,7 @@ export * from './theme'
// composables // composables
export { useSiteData } from './composables/siteData' export { useSiteData } from './composables/siteData'
export { usePageData } from './composables/pageData' export { usePageData } from './composables/pageData'
export { useSiteDataByRoute } from './composables/siteDataByRoute'
export { useRouter, useRoute, Router, Route } from './router' export { useRouter, useRoute, Router, Route } from './router'
// components // components

@ -1,12 +1,13 @@
import { createApp as createClientApp, createSSRApp, ref, readonly } from 'vue' import { createApp as createClientApp, createSSRApp, ref, readonly } from 'vue'
import { createRouter, RouterSymbol } from './router' import { createRouter, RouterSymbol } from './router'
import { useUpdateHead } from './composables/head' import { useUpdateHead } from './composables/head'
import { siteDataRef } from './composables/siteData'
import { pageDataSymbol } from './composables/pageData' import { pageDataSymbol } from './composables/pageData'
import { Content } from './components/Content' import { Content } from './components/Content'
import Debug from './components/Debug.vue' import Debug from './components/Debug.vue'
import Theme from '/@theme/index' import Theme from '/@theme/index'
import { inBrowser, pathToFile } from './utils' import { inBrowser, pathToFile } from './utils'
import { useSiteDataByRoute } from './composables/siteDataByRoute'
import { siteDataRef } from './composables/siteData'
const NotFound = Theme.NotFound || (() => '404 Not Found') const NotFound = Theme.NotFound || (() => '404 Not Found')
@ -15,11 +16,6 @@ export function createApp() {
// distinct per-request. // distinct per-request.
const pageDataRef = ref() const pageDataRef = ref()
if (inBrowser) {
// dynamically update head tags
useUpdateHead(pageDataRef)
}
if (import.meta.hot) { if (import.meta.hot) {
// hot reload pageData // hot reload pageData
import.meta.hot!.on('vitepress:pageData', (data) => { import.meta.hot!.on('vitepress:pageData', (data) => {
@ -80,12 +76,24 @@ export function createApp() {
process.env.NODE_ENV === 'production' ? () => null : Debug process.env.NODE_ENV === 'production' ? () => null : Debug
) )
const siteDataByRouteRef = useSiteDataByRoute(router.route)
if (inBrowser) {
// dynamically update head tags
useUpdateHead(pageDataRef, siteDataByRouteRef)
}
Object.defineProperties(app.config.globalProperties, { Object.defineProperties(app.config.globalProperties, {
$site: { $site: {
get() { get() {
return siteDataRef.value return siteDataRef.value
} }
}, },
$siteByRoute: {
get() {
return siteDataByRouteRef.value
}
},
$page: { $page: {
get() { get() {
return pageDataRef.value return pageDataRef.value
@ -93,7 +101,7 @@ export function createApp() {
}, },
$theme: { $theme: {
get() { get() {
return siteDataRef.value.themeConfig return siteDataByRouteRef.value.themeConfig
} }
} }
}) })
@ -102,7 +110,7 @@ export function createApp() {
Theme.enhanceApp({ Theme.enhanceApp({
app, app,
router, router,
siteData: siteDataRef siteData: siteDataByRouteRef
}) })
} }

@ -1,5 +1,5 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useSiteData } from 'vitepress' import { useSiteDataByRoute } from 'vitepress'
import NavBarLink from './NavBarLink.vue' import NavBarLink from './NavBarLink.vue'
import NavDropdownLink from './NavDropdownLink.vue' import NavDropdownLink from './NavDropdownLink.vue'
@ -14,9 +14,9 @@ export default {
navData: navData:
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production'
? // navbar items do not change in production ? // navbar items do not change in production
useSiteData().value.themeConfig.nav useSiteDataByRoute().value.themeConfig.nav
: // use computed in dev for hot reload : // use computed in dev for hot reload
computed(() => useSiteData().value.themeConfig.nav) computed(() => useSiteDataByRoute().value.themeConfig.nav)
} }
} }
} }

@ -1,4 +1,4 @@
import { useSiteData, usePageData, useRoute } from 'vitepress' import { usePageData, useRoute, useSiteDataByRoute } from 'vitepress'
import { computed, h, FunctionalComponent, VNode } from 'vue' import { computed, h, FunctionalComponent, VNode } from 'vue'
import { Header } from '../../../../types/shared' import { Header } from '../../../../types/shared'
import { isActive, getPathDirName } from '../utils' import { isActive, getPathDirName } from '../utils'
@ -31,7 +31,7 @@ export default {
setup() { setup() {
const pageData = usePageData() const pageData = usePageData()
const siteData = useSiteData() const siteData = useSiteDataByRoute()
const route = useRoute() const route = useRoute()
useActiveSidebarLinks() useActiveSidebarLinks()

@ -2,15 +2,22 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"outDir": "../../dist/client", "outDir": "../../dist",
"module": "esnext", "module": "esnext",
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],
"types": ["vite"], "types": ["vite"],
"paths": { "paths": {
"/@app/*": ["app/*"], "/@app/*": ["app/*"],
"/@theme/*": ["theme-default/*"], "/@theme/*": ["theme-default/*"],
"/@shared/*": ["../shared/*"],
"vitepress": ["app/exports.ts"] "vitepress": ["app/exports.ts"]
} }
}, },
"include": [".", "../../types/shared.d.ts"] "include": [
".",
"../../types/shared.d.ts",
],
"exclude": [
"../shared"
]
} }

@ -1,6 +1,6 @@
import path from 'path' import path from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import { SiteConfig } from '../config' import { SiteConfig, resolveSiteDataByRoute } from '../config'
import { HeadConfig } from '../../../types/shared' import { HeadConfig } from '../../../types/shared'
import { BuildResult } from 'vite' import { BuildResult } from 'vite'
import { OutputChunk, OutputAsset } from 'rollup' import { OutputChunk, OutputAsset } from 'rollup'
@ -19,6 +19,7 @@ export async function renderPage(
const { createApp } = require(path.join(config.tempDir, 'app.js')) const { createApp } = require(path.join(config.tempDir, 'app.js'))
const { app, router } = createApp() const { app, router } = createApp()
const routePath = `/${page.replace(/\.md$/, '')}` const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath)
router.go(routePath) router.go(routePath)
// lazy require server-renderer for production build // lazy require server-renderer for production build
const content = await require('@vue/server-renderer').renderToString(app) const content = await require('@vue/server-renderer').renderToString(app)
@ -38,7 +39,7 @@ export async function renderPage(
)) ))
const pageData = JSON.parse(__pageData) const pageData = JSON.parse(__pageData)
const assetPath = `${config.site.base}_assets/` const assetPath = `${siteData.base}_assets/`
const preloadLinks = [ const preloadLinks = [
// resolve imports for index.js + page.md.js and inject script tags for // resolve imports for index.js + page.md.js and inject script tags for
// them as well so we fetch everything as early as possible without having // them as well so we fetch everything as early as possible without having
@ -53,15 +54,15 @@ export async function renderPage(
.join('\n ') .join('\n ')
const html = ` const html = `
<html lang="en-US"> <html lang="${siteData.lang}">
<head> <head>
<title>${pageData.title ? pageData.title + ` | ` : ``}${ <title>${pageData.title ? pageData.title + ` | ` : ``}${
config.site.title siteData.title
}</title> }</title>
<meta name="description" content="${config.site.description}"> <meta name="description" content="${siteData.description}">
<link rel="stylesheet" href="${assetPath}${cssChunk.fileName}"> <link rel="stylesheet" href="${assetPath}${cssChunk.fileName}">
${preloadLinks} ${preloadLinks}
${renderHead(config.site.head)} ${renderHead(siteData.head)}
${renderHead(pageData.frontmatter.head)} ${renderHead(pageData.frontmatter.head)}
</head> </head>
<body> <body>

@ -4,16 +4,19 @@ import chalk from 'chalk'
import globby from 'globby' import globby from 'globby'
import { createResolver, APP_PATH } from './resolver' import { createResolver, APP_PATH } from './resolver'
import { Resolver } from 'vite' import { Resolver } from 'vite'
import { SiteData, HeadConfig } from '../../types/shared' import { SiteData, HeadConfig, LocaleConfig } from '../../types/shared'
export { resolveSiteDataByRoute } from '../shared/config'
const debug = require('debug')('vitepress:config') const debug = require('debug')('vitepress:config')
export interface UserConfig<ThemeConfig = any> { export interface UserConfig<ThemeConfig = any> {
lang?: string
base?: string base?: string
title?: string title?: string
description?: string description?: string
head?: HeadConfig[] head?: HeadConfig[]
themeConfig?: ThemeConfig themeConfig?: ThemeConfig
locales?: Record<string, LocaleConfig>
// TODO locales support etc. // TODO locales support etc.
} }
@ -70,10 +73,12 @@ export async function resolveSiteData(root: string): Promise<SiteData> {
} }
return { return {
lang: userConfig.lang || 'en-US',
title: userConfig.title || 'VitePress', title: userConfig.title || 'VitePress',
description: userConfig.description || 'A VitePress site', description: userConfig.description || 'A VitePress site',
base: userConfig.base ? userConfig.base.replace(/([^/])$/, '$1/') : '/', base: userConfig.base ? userConfig.base.replace(/([^/])$/, '$1/') : '/',
head: userConfig.head || [], head: userConfig.head || [],
themeConfig: userConfig.themeConfig || {} themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {}
} }
} }

@ -2,6 +2,7 @@ import path from 'path'
import { Resolver } from 'vite' import { Resolver } from 'vite'
export const APP_PATH = path.join(__dirname, '../client/app') export const APP_PATH = path.join(__dirname, '../client/app')
export const SHARED_PATH = path.join(__dirname, '../client/shared')
// special virtual file // special virtual file
// we can't directly import '/@siteData' becase // we can't directly import '/@siteData' becase
@ -19,6 +20,7 @@ export function createResolver(themeDir: string): Resolver {
alias: { alias: {
'/@app/': APP_PATH, '/@app/': APP_PATH,
'/@theme/': themeDir, '/@theme/': themeDir,
'/@shared/': SHARED_PATH,
vitepress: '/@app/exports.js', vitepress: '/@app/exports.js',
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH [SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
}, },

@ -1,10 +1,10 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../../dist/node", "outDir": "../../dist",
"module": "commonjs", "module": "commonjs",
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],
"sourceMap": true "sourceMap": true
}, },
"include": [".", "../../types/shared.d.ts"] "include": [".", "../shared", "../../types/shared.d.ts"]
} }

@ -0,0 +1,48 @@
import { SiteData } from '../../types/shared'
function findMatchRoot(route: string, roots: string[]) {
// first match to the routes with the most deep level.
roots.sort((a, b) => {
const levelDelta = b.split('/').length - a.split('/').length
if (levelDelta !== 0) {
return levelDelta
} else {
return b.length - a.length
}
})
for (const r of roots) {
if (route.startsWith(r)) return r
}
return undefined
}
function resolveLocales<T>(
locales: Record<string, T>,
route: string
): T | undefined {
const localeRoot = findMatchRoot(route, Object.keys(locales))
return localeRoot ? locales[localeRoot] : undefined
}
// this merges the locales data to the main data by the route
export function resolveSiteDataByRoute(siteData: SiteData, route: string) {
const localeData = resolveLocales(siteData.locales || {}, route) || {}
const localeThemeConfig =
resolveLocales<any>(
(siteData.themeConfig && siteData.themeConfig.locales) || {},
route
) || {}
return {
...siteData,
...localeData,
themeConfig: {
...siteData.themeConfig,
...localeThemeConfig,
// clean the locales to reduce the bundle size
locales: {}
},
locales: {}
}
}

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "../../dist/client/shared",
"module": "esnext",
"lib": ["ESNext", "DOM"],
},
"include": [
".",
"../../types/shared.d.ts",
]
}

9
types/shared.d.ts vendored

@ -1,11 +1,20 @@
// types shared between server and client. // types shared between server and client.
export interface LocaleConfig {
lang: string
title?: string
description?: string
head?: HeadConfig[]
}
export interface SiteData<ThemeConfig = any> { export interface SiteData<ThemeConfig = any> {
lang: string
title: string title: string
description: string description: string
base: string base: string
head: HeadConfig[] head: HeadConfig[]
themeConfig: ThemeConfig themeConfig: ThemeConfig
locales: Record<string, LocaleConfig>
} }
export type HeadConfig = export type HeadConfig =

Loading…
Cancel
Save