From 7802cb55c2a82cc1878fc1ebc4dc2fcf1f2f1ff0 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Wed, 22 Jul 2020 04:49:50 +0800 Subject: [PATCH] feat: i18n support (#50) --- package.json | 5 +- src/client/app/components/Debug.vue | 11 +++-- src/client/app/composables/head.ts | 12 +++-- src/client/app/composables/siteDataByRoute.ts | 10 ++++ src/client/app/exports.ts | 1 + src/client/app/index.ts | 24 ++++++---- src/client/theme-default/components/NavBar.ts | 6 +-- .../theme-default/components/SideBar.ts | 4 +- src/client/tsconfig.json | 11 ++++- src/node/build/render.ts | 13 ++--- src/node/config.ts | 9 +++- src/node/resolver.ts | 2 + src/node/tsconfig.json | 4 +- src/shared/config.ts | 48 +++++++++++++++++++ src/shared/tsconfig.json | 13 +++++ types/shared.d.ts | 9 ++++ 16 files changed, 146 insertions(+), 36 deletions(-) create mode 100644 src/client/app/composables/siteDataByRoute.ts create mode 100644 src/shared/config.ts create mode 100644 src/shared/tsconfig.json diff --git a/package.json b/package.json index 19a750ec..7d207704 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,12 @@ }, "homepage": "https://github.com/vuejs/vitepress/tree/master/#readme", "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-copy": "node scripts/watchAndCopy", "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", "prepublishOnly": "yarn build && yarn changelog", "postpublish": "git add CHANGELOG.md && git commit -m 'chore: changelog [ci skip]'" diff --git a/src/client/app/components/Debug.vue b/src/client/app/components/Debug.vue index 003e89d6..5f729207 100644 --- a/src/client/app/components/Debug.vue +++ b/src/client/app/components/Debug.vue @@ -1,8 +1,9 @@ @@ -27,8 +28,8 @@ export default { cursor: pointer; bottom: 0; right: 0; - width: 50px; - height: 20px; + width: 80px; + height: 30px; padding: 5px; overflow: hidden; color: #eeeeee; @@ -40,7 +41,7 @@ export default { width: 500px; height: 100%; margin-top: 0; - padding: 5px 20px; + padding: 0 0; overflow: scroll; } @@ -48,5 +49,7 @@ export default { font-family: Hack, monospace; font-size: 13px; margin: 0; + padding: 5px 10px; + border-bottom: 1px solid #eee; } diff --git a/src/client/app/composables/head.ts b/src/client/app/composables/head.ts index 7ccd7e6e..448e3d26 100644 --- a/src/client/app/composables/head.ts +++ b/src/client/app/composables/head.ts @@ -1,9 +1,11 @@ -import { watchEffect } from 'vue' -import { siteDataRef } from './siteData' +import { watchEffect, Ref } from 'vue' 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 +) { const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta')) let isFirstUpdate = true @@ -27,7 +29,7 @@ export function useUpdateHead(pageDataRef: PageDataRef) { watchEffect(() => { const pageData = pageDataRef.value - const siteData = siteDataRef.value + const siteData = siteDataByRouteRef.value const pageTitle = pageData && pageData.title document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title updateHeadTags([ diff --git a/src/client/app/composables/siteDataByRoute.ts b/src/client/app/composables/siteDataByRoute.ts new file mode 100644 index 00000000..f7bcb3a0 --- /dev/null +++ b/src/client/app/composables/siteDataByRoute.ts @@ -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) + }) +} diff --git a/src/client/app/exports.ts b/src/client/app/exports.ts index f5d84533..2eff109e 100644 --- a/src/client/app/exports.ts +++ b/src/client/app/exports.ts @@ -7,6 +7,7 @@ export * from './theme' // composables export { useSiteData } from './composables/siteData' export { usePageData } from './composables/pageData' +export { useSiteDataByRoute } from './composables/siteDataByRoute' export { useRouter, useRoute, Router, Route } from './router' // components diff --git a/src/client/app/index.ts b/src/client/app/index.ts index c3909ccc..12108031 100644 --- a/src/client/app/index.ts +++ b/src/client/app/index.ts @@ -1,12 +1,13 @@ import { createApp as createClientApp, createSSRApp, ref, readonly } from 'vue' import { createRouter, RouterSymbol } from './router' import { useUpdateHead } from './composables/head' -import { siteDataRef } from './composables/siteData' import { pageDataSymbol } from './composables/pageData' import { Content } from './components/Content' import Debug from './components/Debug.vue' import Theme from '/@theme/index' import { inBrowser, pathToFile } from './utils' +import { useSiteDataByRoute } from './composables/siteDataByRoute' +import { siteDataRef } from './composables/siteData' const NotFound = Theme.NotFound || (() => '404 Not Found') @@ -15,11 +16,6 @@ export function createApp() { // distinct per-request. const pageDataRef = ref() - if (inBrowser) { - // dynamically update head tags - useUpdateHead(pageDataRef) - } - if (import.meta.hot) { // hot reload pageData import.meta.hot!.on('vitepress:pageData', (data) => { @@ -80,12 +76,24 @@ export function createApp() { 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, { $site: { get() { return siteDataRef.value } }, + $siteByRoute: { + get() { + return siteDataByRouteRef.value + } + }, $page: { get() { return pageDataRef.value @@ -93,7 +101,7 @@ export function createApp() { }, $theme: { get() { - return siteDataRef.value.themeConfig + return siteDataByRouteRef.value.themeConfig } } }) @@ -102,7 +110,7 @@ export function createApp() { Theme.enhanceApp({ app, router, - siteData: siteDataRef + siteData: siteDataByRouteRef }) } diff --git a/src/client/theme-default/components/NavBar.ts b/src/client/theme-default/components/NavBar.ts index a5fba6f2..e41aa966 100644 --- a/src/client/theme-default/components/NavBar.ts +++ b/src/client/theme-default/components/NavBar.ts @@ -1,5 +1,5 @@ import { computed } from 'vue' -import { useSiteData } from 'vitepress' +import { useSiteDataByRoute } from 'vitepress' import NavBarLink from './NavBarLink.vue' import NavDropdownLink from './NavDropdownLink.vue' @@ -14,9 +14,9 @@ export default { navData: process.env.NODE_ENV === 'production' ? // navbar items do not change in production - useSiteData().value.themeConfig.nav + useSiteDataByRoute().value.themeConfig.nav : // use computed in dev for hot reload - computed(() => useSiteData().value.themeConfig.nav) + computed(() => useSiteDataByRoute().value.themeConfig.nav) } } } diff --git a/src/client/theme-default/components/SideBar.ts b/src/client/theme-default/components/SideBar.ts index 54dbfa67..fff731e2 100644 --- a/src/client/theme-default/components/SideBar.ts +++ b/src/client/theme-default/components/SideBar.ts @@ -1,4 +1,4 @@ -import { useSiteData, usePageData, useRoute } from 'vitepress' +import { usePageData, useRoute, useSiteDataByRoute } from 'vitepress' import { computed, h, FunctionalComponent, VNode } from 'vue' import { Header } from '../../../../types/shared' import { isActive, getPathDirName } from '../utils' @@ -31,7 +31,7 @@ export default { setup() { const pageData = usePageData() - const siteData = useSiteData() + const siteData = useSiteDataByRoute() const route = useRoute() useActiveSidebarLinks() diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index d9838829..b004aa66 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -2,15 +2,22 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "baseUrl": ".", - "outDir": "../../dist/client", + "outDir": "../../dist", "module": "esnext", "lib": ["ESNext", "DOM"], "types": ["vite"], "paths": { "/@app/*": ["app/*"], "/@theme/*": ["theme-default/*"], + "/@shared/*": ["../shared/*"], "vitepress": ["app/exports.ts"] } }, - "include": [".", "../../types/shared.d.ts"] + "include": [ + ".", + "../../types/shared.d.ts", + ], + "exclude": [ + "../shared" + ] } diff --git a/src/node/build/render.ts b/src/node/build/render.ts index 14d67efd..51ad2c39 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -1,6 +1,6 @@ import path from 'path' import fs from 'fs-extra' -import { SiteConfig } from '../config' +import { SiteConfig, resolveSiteDataByRoute } from '../config' import { HeadConfig } from '../../../types/shared' import { BuildResult } from 'vite' import { OutputChunk, OutputAsset } from 'rollup' @@ -19,6 +19,7 @@ export async function renderPage( const { createApp } = require(path.join(config.tempDir, 'app.js')) const { app, router } = createApp() const routePath = `/${page.replace(/\.md$/, '')}` + const siteData = resolveSiteDataByRoute(config.site, routePath) router.go(routePath) // lazy require server-renderer for production build const content = await require('@vue/server-renderer').renderToString(app) @@ -38,7 +39,7 @@ export async function renderPage( )) const pageData = JSON.parse(__pageData) - const assetPath = `${config.site.base}_assets/` + const assetPath = `${siteData.base}_assets/` const preloadLinks = [ // 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 @@ -53,15 +54,15 @@ export async function renderPage( .join('\n ') const html = ` - + ${pageData.title ? pageData.title + ` | ` : ``}${ - config.site.title + siteData.title } - + ${preloadLinks} - ${renderHead(config.site.head)} + ${renderHead(siteData.head)} ${renderHead(pageData.frontmatter.head)} diff --git a/src/node/config.ts b/src/node/config.ts index 39effaa1..ecc5248a 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -4,16 +4,19 @@ import chalk from 'chalk' import globby from 'globby' import { createResolver, APP_PATH } from './resolver' 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') export interface UserConfig { + lang?: string base?: string title?: string description?: string head?: HeadConfig[] themeConfig?: ThemeConfig + locales?: Record // TODO locales support etc. } @@ -70,10 +73,12 @@ export async function resolveSiteData(root: string): Promise { } return { + lang: userConfig.lang || 'en-US', title: userConfig.title || 'VitePress', description: userConfig.description || 'A VitePress site', base: userConfig.base ? userConfig.base.replace(/([^/])$/, '$1/') : '/', head: userConfig.head || [], - themeConfig: userConfig.themeConfig || {} + themeConfig: userConfig.themeConfig || {}, + locales: userConfig.locales || {} } } diff --git a/src/node/resolver.ts b/src/node/resolver.ts index 8576b016..9ba866ac 100644 --- a/src/node/resolver.ts +++ b/src/node/resolver.ts @@ -2,6 +2,7 @@ import path from 'path' import { Resolver } from 'vite' export const APP_PATH = path.join(__dirname, '../client/app') +export const SHARED_PATH = path.join(__dirname, '../client/shared') // special virtual file // we can't directly import '/@siteData' becase @@ -19,6 +20,7 @@ export function createResolver(themeDir: string): Resolver { alias: { '/@app/': APP_PATH, '/@theme/': themeDir, + '/@shared/': SHARED_PATH, vitepress: '/@app/exports.js', [SITE_DATA_ID]: SITE_DATA_REQUEST_PATH }, diff --git a/src/node/tsconfig.json b/src/node/tsconfig.json index 25751d5a..e67dbbc7 100644 --- a/src/node/tsconfig.json +++ b/src/node/tsconfig.json @@ -1,10 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "../../dist/node", + "outDir": "../../dist", "module": "commonjs", "lib": ["ESNext", "DOM"], "sourceMap": true }, - "include": [".", "../../types/shared.d.ts"] + "include": [".", "../shared", "../../types/shared.d.ts"] } diff --git a/src/shared/config.ts b/src/shared/config.ts new file mode 100644 index 00000000..2480f751 --- /dev/null +++ b/src/shared/config.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( + locales: Record, + 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( + (siteData.themeConfig && siteData.themeConfig.locales) || {}, + route + ) || {} + + return { + ...siteData, + ...localeData, + themeConfig: { + ...siteData.themeConfig, + ...localeThemeConfig, + // clean the locales to reduce the bundle size + locales: {} + }, + locales: {} + } +} diff --git a/src/shared/tsconfig.json b/src/shared/tsconfig.json new file mode 100644 index 00000000..34954d27 --- /dev/null +++ b/src/shared/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "../../dist/client/shared", + "module": "esnext", + "lib": ["ESNext", "DOM"], + }, + "include": [ + ".", + "../../types/shared.d.ts", + ] +} diff --git a/types/shared.d.ts b/types/shared.d.ts index 64724d8e..93a77742 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -1,11 +1,20 @@ // types shared between server and client. +export interface LocaleConfig { + lang: string + title?: string + description?: string + head?: HeadConfig[] +} + export interface SiteData { + lang: string title: string description: string base: string head: HeadConfig[] themeConfig: ThemeConfig + locales: Record } export type HeadConfig =