feat: complete language switching functionality and centralized page routing

This commit is contained in:
radishzzz 2025-03-14 03:30:56 +00:00
parent 4651828dd1
commit 05d3a8034b
26 changed files with 253 additions and 146 deletions

View file

@ -1,12 +1,13 @@
// Astro integrations
import mdx from '@astrojs/mdx'
import partytown from '@astrojs/partytown'
import sitemap from '@astrojs/sitemap'
import { transformerCopyButton } from '@rehype-pretty/transformers'
import compress from 'astro-compress'
import robotsTxt from 'astro-robots-txt'
import { defineConfig } from 'astro/config'
// Rehype plugins
// Rehype plugins (HTML processors)
import { defineConfig } from 'astro/config'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeComponents from 'rehype-components'
import rehypeExternalLinks from 'rehype-external-links'
@ -14,21 +15,21 @@ import rehypeKatex from 'rehype-katex'
import rehypePrettyCode from 'rehype-pretty-code'
import rehypeSlug from 'rehype-slug'
// Remark plugins
// Remark plugins (Markdown processors)
import remarkDirective from 'remark-directive'
import remarkGithubAdmonitionsToDirectives from 'remark-github-admonitions-to-directives'
import remarkMath from 'remark-math'
import remarkSectionize from 'remark-sectionize'
import UnoCSS from 'unocss/astro'
import { themeConfig } from './src/config.js'
// Local plugins
import { AdmonitionComponent } from './src/plugins/rehype-component-admonition.js'
import { GithubCardComponent } from './src/plugins/rehype-component-github-card.js'
import { parseDirectiveNode } from './src/plugins/remark-directive-rehype.js'
import { remarkExcerpt } from './src/plugins/remark-excerpt.js'
import { remarkReadingTime } from './src/plugins/remark-reading-time.js'
import { langMap } from './src/utils/i18n/ui.js'
// Project configuration and utilities
import UnoCSS from 'unocss/astro'
import { themeConfig } from './src/config'
import { langMap } from './src/i18n/ui'
import { AdmonitionComponent } from './src/plugins/rehype-component-admonition'
import { GithubCardComponent } from './src/plugins/rehype-component-github-card'
import { parseDirectiveNode } from './src/plugins/remark-directive-rehype'
import { remarkExcerpt } from './src/plugins/remark-excerpt'
import { remarkReadingTime } from './src/plugins/remark-reading-time'
const url = themeConfig.site.url
const { light, dark } = themeConfig.color

8
pnpm-lock.yaml generated
View file

@ -1623,8 +1623,8 @@ packages:
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
electron-to-chromium@1.5.117:
resolution: {integrity: sha512-G4+CYIJBiQ72N0gi868tmG4WsD8bwLE9XytBdfgXO5zdlTlvOP2ABzWYILYxCIHmsbm2HjBSgm/E/H/QfcnIyQ==}
electron-to-chromium@1.5.118:
resolution: {integrity: sha512-yNDUus0iultYyVoEFLnQeei7LOQkL8wg8GQpkPCRrOlJXlcCwa6eGKZkxQ9ciHsqZyYbj8Jd94X1CTPzGm+uIA==}
emmet@2.4.11:
resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==}
@ -5221,7 +5221,7 @@ snapshots:
browserslist@4.24.4:
dependencies:
caniuse-lite: 1.0.30001704
electron-to-chromium: 1.5.117
electron-to-chromium: 1.5.118
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.4)
@ -5480,7 +5480,7 @@ snapshots:
duplexer@0.1.2: {}
electron-to-chromium@1.5.117: {}
electron-to-chromium@1.5.118: {}
emmet@2.4.11:
dependencies:

View file

@ -1,6 +1,6 @@
---
import { themeConfig } from '@/config'
import { getWalineLang } from '@/utils/i18n/ui'
import { getWalineLang } from '@/i18n/ui'
const {
serverURL = '',
@ -33,8 +33,7 @@ const {
>
</div>
<!-- Not use 'is:inline' or 'define:vars' -->
<!-- 'define:vars' ≈ 'is:inline' -->
<!-- Not use is:inline or define:vars -->
<script>
import { init } from '@waline/client'
import '@waline/client/style'

View file

@ -5,9 +5,10 @@ import themeConfig from '@/config'
interface Props {
class?: string
supportedLangs?: string[] // 文章支持的语言列表
}
const { class: className } = Astro.props
const { class: className, supportedLangs = [] } = Astro.props
const { author } = themeConfig.site
const { links, startYear } = themeConfig.footer
@ -26,7 +27,7 @@ const year = Number(startYear) === currentYear
<!-- only show on desktop -->
<div class="mb-11.5 ml-1.5 hidden gap-7 lg:flex">
<ThemeToggle />
<LanguageSwitcher />
<LanguageSwitcher supportedLangs={supportedLangs} />
</div>
<p>

View file

@ -1,6 +1,6 @@
---
import themeConfig from '@/config'
import { getPagePath } from '@/utils/i18n/path'
import { getPagePath } from '@/i18n/path'
const { title, subtitle } = themeConfig.site
const { titleSpace } = themeConfig.global

View file

@ -1,6 +1,6 @@
---
import themeConfig from '@/config'
import { getPagePath } from '@/utils/i18n/path'
import { getPagePath } from '@/i18n/path'
const { title, subtitle } = themeConfig.site
const { titleSpace } = themeConfig.global

View file

@ -1,6 +1,6 @@
---
import { getPagePath } from '@/utils/i18n/path'
import { ui } from '@/utils/i18n/ui'
import { getPagePath } from '@/i18n/path'
import { ui } from '@/i18n/ui'
const currentPath = Astro.url.pathname
const { currentLang, isHome, isPost, isTag, isAbout, getLocalizedPath }

View file

@ -1,6 +1,6 @@
---
import { themeConfig } from '@/config'
import { isPostPage } from '@/utils/i18n/path'
import { isPostPage } from '@/i18n/path'
interface Props {
date: Date

View file

@ -1,11 +1,19 @@
---
import { getNextLangUrl } from '@/utils/i18n/lang'
import { getNextLangUrl, getPostNextLangUrl } from '@/i18n/lang'
import { isPostPage } from '@/i18n/path'
// 获取当前路径
interface Props {
supportedLangs?: string[] // 文章支持的语言列表
}
const { supportedLangs = [] } = Astro.props
const currentPath = Astro.url.pathname
const isPost = isPostPage(currentPath)
// 直接获取下一个语言的URL
const nextUrl = getNextLangUrl(currentPath)
// 根据页面类型选择不同的URL获取函数
const nextUrl = isPost && supportedLangs.length > 0
? getPostNextLangUrl(currentPath, supportedLangs)
: getNextLangUrl(currentPath)
---
<a

View file

@ -1,4 +1,8 @@
import { themeConfig } from '@/config'
import themeConfig from '@/config'
// 从配置中获取默认语言和更多语言配置
const defaultLocale = themeConfig.global.locale
// const moreLocale = themeConfig.global.moreLocale
/**
*
@ -89,6 +93,24 @@ export function getLangFromPath(currentPath: string): string {
return currentLang
}
/**
*
* @param lang
* @returns
*/
export function getSupportedLangs(lang?: string): string[] {
const defaultLocale = themeConfig.global.locale
const allLocales = [defaultLocale, ...themeConfig.global.moreLocale]
// 如果指定了语言且不为空
if (lang && typeof lang === 'string' && lang.trim() !== '') {
return [lang]
}
// 否则返回所有支持的语言
return allLocales
}
/**
* URL
* @param currentPath
@ -104,3 +126,36 @@ export function getNextLangUrl(currentPath: string): string {
// 构建下一个语言的URL
return buildNextLangUrl(currentPath, currentLang, nextLang)
}
/**
* URL
* @param currentPath
* @param supportedLangs
* @returns URL
*/
export function getPostNextLangUrl(currentPath: string, supportedLangs: string[]): string {
// 从路径提取当前语言
const currentLang = getLangFromPath(currentPath)
// 如果没有提供支持的语言或列表为空,使用普通的语言切换
if (!supportedLangs || supportedLangs.length === 0) {
return getNextLangUrl(currentPath)
}
// 找到当前语言在支持的语言中的索引
const currentIndex = supportedLangs.indexOf(currentLang)
// 如果当前语言不在支持的语言中,或者路径是根路径,返回第一个支持的语言
if (currentIndex === -1 || currentPath === '/') {
const nextLang = supportedLangs[0]
// 如果下一个语言是默认语言,返回根路径
return nextLang === defaultLocale ? '/' : `/${nextLang}/`
}
// 计算下一个语言的索引
const nextIndex = (currentIndex + 1) % supportedLangs.length
const nextLang = supportedLangs[nextIndex]
// 构建下一个语言的URL
return buildNextLangUrl(currentPath, currentLang, nextLang)
}

131
src/i18n/route.ts Normal file
View file

@ -0,0 +1,131 @@
import type { CollectionEntry } from 'astro:content'
import { themeConfig } from '@/config'
// 默认语言和更多语言
const defaultLocale = themeConfig.global.locale
const moreLocale = themeConfig.global.moreLocale
// 所有支持的语言
const allLocales = [defaultLocale, ...moreLocale]
// 生成默认语言标签页面的路径配置
export function generateTagPaths(tags: string[]) {
return tags.map(tag => ({
params: { tag },
props: { tag },
}))
}
// 生成默认语言文章页面的路径配置
export function generatePostPaths(posts: CollectionEntry<'posts'>[]) {
// 创建slug到语言的映射
const slugToLangs: Record<string, string[]> = {}
// 填充映射
posts.forEach((post) => {
const slug = post.data.abbrlink || post.slug
const lang = post.data.lang || defaultLocale
// 如果文章没有指定语言,初始化为所有支持的语言
if (!slugToLangs[slug]) {
if (!post.data.lang) {
slugToLangs[slug] = [...allLocales] // 文章支持所有语言
}
else {
slugToLangs[slug] = [defaultLocale] // 仅默认语言和指定语言
}
}
if (!slugToLangs[slug].includes(lang)) {
slugToLangs[slug].push(lang)
}
})
return posts.map(post => ({
params: {
slug: post.data.abbrlink || post.slug,
},
props: {
post,
supportedLangs: slugToLangs[post.data.abbrlink || post.slug] || [],
},
}))
}
// 生成更多语言静态路径
export function generateMultiLangPaths() {
return moreLocale.map(lang => ({
params: { lang },
}))
}
// 生成更多语言标签页面的路径配置
export function generateMultiLangTagPaths(tags: string[]) {
return moreLocale.flatMap(lang => (
tags.map(tag => ({
params: { lang, tag },
props: { tag },
}))
))
}
// 生成更多语言文章页面的路径配置
export function generateMultiLangPostPaths(posts: CollectionEntry<'posts'>[]) {
// 创建slug到语言的映射
const slugToLangs: Record<string, string[]> = {}
// 填充映射
posts.forEach((post) => {
const slug = post.data.abbrlink || post.slug
const lang = post.data.lang || defaultLocale
// 如果文章没有指定语言,初始化为所有支持的语言
if (!slugToLangs[slug]) {
if (!post.data.lang) {
slugToLangs[slug] = [...allLocales] // 文章支持所有语言
}
else {
slugToLangs[slug] = [defaultLocale] // 仅默认语言和指定语言
}
}
if (!slugToLangs[slug].includes(lang)) {
slugToLangs[slug].push(lang)
}
})
interface PathResult {
params: {
lang: string
slug: string
}
props: {
post: CollectionEntry<'posts'>
supportedLangs: string[]
}
}
return posts.flatMap((post) => {
const result: PathResult[] = []
const slug = post.data.abbrlink || post.slug
// 确定文章的语言支持
const postLang = post.data.lang && typeof post.data.lang === 'string' && post.data.lang.trim() !== ''
? [post.data.lang]
: moreLocale
// 获取这篇文章支持的所有语言
const supportedLangs = slugToLangs[slug] || []
// 添加非默认语言路径
postLang.forEach((lang) => {
if (lang !== defaultLocale) {
result.push({
params: { lang, slug },
props: { post, supportedLangs },
})
}
})
return result
})
}

View file

@ -9,8 +9,8 @@ import LanguageSwitcher from '@/components/Widgets/LanguageSwitcher.astro'
// import Scrollbar from '@/components/Scrollbar.astro'
import ThemeToggle from '@/components/Widgets/ThemeToggle.astro'
import themeConfig from '@/config'
import { getPagePath } from '@/i18n/path'
import Head from '@/layouts/Head.astro'
import { getPagePath } from '@/utils/i18n/path'
import '@/styles/font.css'
import '@/styles/global.css'
@ -20,9 +20,10 @@ interface Props {
postTitle?: string
postDescription?: string
postSlug?: string
supportedLangs?: string[]
}
const { postTitle, postDescription, postSlug } = Astro.props
const { postTitle, postDescription, postSlug, supportedLangs = [] } = Astro.props
const { isHome, isPost } = getPagePath(Astro.url.pathname)
const { light: { background: lightMode }, dark: { background: darkMode } } = themeConfig.color
const fontStyle = themeConfig.global.fontStyle === 'serif' ? 'font-serif' : 'font-sans'
@ -55,7 +56,7 @@ const footerMarginClass = isPost && themeConfig.comment?.waline?.serverURL
<MainHeader />
<Navigation />
<!-- only show footer on desktop-->
<Footer class="fixed hidden lg:block" />
<Footer class="fixed hidden lg:block" supportedLangs={supportedLangs} />
</div>
<!-- show simple header on mobile for post pages -->
@ -67,7 +68,7 @@ const footerMarginClass = isPost && themeConfig.comment?.waline?.serverURL
</main>
<!-- show footer on mobile -->
<Footer class={`block lg:hidden ${footerMarginClass}`} />
<Footer class={`block lg:hidden ${footerMarginClass}`} supportedLangs={supportedLangs} />
</div>
<!-- <Scrollbar /> -->
<!-- <BackToTop /> -->
@ -75,7 +76,7 @@ const footerMarginClass = isPost && themeConfig.comment?.waline?.serverURL
<!-- only show on mobile (fix position issue on ios / fix right distance)-->
<div class="absolute right-7.25vw top-13.5 flex gap-6 [@supports(-webkit-touch-callout:none)]:top-12.5 min-[823px]:right-[calc(50vw-22rem)] lg:hidden">
<LanguageSwitcher />
<LanguageSwitcher supportedLangs={supportedLangs} />
<ThemeToggle />
</div>

View file

@ -1,9 +1,9 @@
---
import { generateMultiLangPaths } from '@/i18n/route'
import Layout from '@/layouts/Layout.astro'
import { generateLanguagePaths } from '@/utils/i18n/route'
export function getStaticPaths() {
return generateLanguagePaths()
return generateMultiLangPaths()
}
---

View file

@ -1,5 +1,6 @@
import type { APIContext } from 'astro'
import themeConfig from '@/config'
import { generateMultiLangPaths } from '@/i18n/route'
import { generateRSS } from '@/utils/rss'
const { moreLocale } = themeConfig.global
@ -9,7 +10,7 @@ type SupportedLanguage = typeof moreLocale[number]
// Generate static paths for all supported languages
export function getStaticPaths() {
return moreLocale.map((lang: SupportedLanguage) => ({ params: { lang } }))
return generateMultiLangPaths()
}
export async function GET({ params }: APIContext) {

View file

@ -1,13 +1,11 @@
---
import PostList from '@/components/PostList.astro'
import { themeConfig } from '@/config'
import { generateMultiLangPaths } from '@/i18n/route'
import Layout from '@/layouts/Layout.astro'
import { getPinnedPosts, getPostsByYear } from '@/utils/content'
export function getStaticPaths() {
return themeConfig.global.moreLocale.map(lang => ({
params: { lang },
}))
return generateMultiLangPaths()
}
const { lang } = Astro.params

View file

@ -1,9 +1,9 @@
---
import Waline from '@/components/Comments/Waline.astro'
import { generateMultiLangPostPaths } from '@/i18n/route'
import Layout from '@/layouts/Layout.astro'
import { checkSlugDuplication } from '@/utils/content'
import { generateDescription } from '@/utils/description'
import { generateMultiLangPostPaths } from '@/utils/i18n/route'
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
@ -17,7 +17,7 @@ export async function getStaticPaths() {
return generateMultiLangPostPaths(posts)
}
const { post } = Astro.props
const { post, supportedLangs = [] } = Astro.props
const description = generateDescription(post)
const { Content, remarkPluginFrontmatter } = await post.render()
---
@ -26,6 +26,7 @@ const { Content, remarkPluginFrontmatter } = await post.render()
postTitle={post.data.title}
postDescription={description}
postSlug={post.slug}
supportedLangs={supportedLangs}
>
<article>
<h1>{post.data.title}</h1>

View file

@ -1,5 +1,6 @@
import type { APIContext } from 'astro'
import themeConfig from '@/config'
import { generateMultiLangPaths } from '@/i18n/route'
import { generateRSS } from '@/utils/rss'
const { moreLocale } = themeConfig.global
@ -9,7 +10,7 @@ type SupportedLanguage = typeof moreLocale[number]
// Generate static paths for all supported languages
export function getStaticPaths() {
return moreLocale.map((lang: SupportedLanguage) => ({ params: { lang } }))
return generateMultiLangPaths()
}
export async function GET({ params }: APIContext) {

View file

@ -1,7 +1,7 @@
---
import { generateMultiLangTagPaths } from '@/i18n/route'
import Layout from '@/layouts/Layout.astro'
import { getAllTags, getPostsByTag } from '@/utils/content'
import { generateMultiLangTagPaths } from '@/utils/i18n/route'
export async function getStaticPaths() {
const tags = await getAllTags()

View file

@ -1,10 +1,10 @@
---
import { generateMultiLangPaths } from '@/i18n/route'
import Layout from '@/layouts/Layout.astro'
import { getAllTags } from '@/utils/content'
import { generateLanguagePaths } from '@/utils/i18n/route'
export function getStaticPaths() {
return generateLanguagePaths()
return generateMultiLangPaths()
}
const { lang } = Astro.params

View file

@ -1,10 +1,10 @@
---
import Comments from '@/components/Comments/index.astro'
import PostTime from '@/components/PostTime.astro'
import { generatePostPaths } from '@/i18n/route'
import Layout from '@/layouts/Layout.astro'
import { checkSlugDuplication } from '@/utils/content'
import { generateDescription } from '@/utils/description'
import { generatePostPaths } from '@/utils/i18n/route'
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
@ -18,7 +18,7 @@ export async function getStaticPaths() {
return generatePostPaths(posts)
}
const { post } = Astro.props
const { post, supportedLangs = [] } = Astro.props
const description = generateDescription(post)
const { Content, remarkPluginFrontmatter } = await post.render()
---
@ -27,6 +27,7 @@ const { Content, remarkPluginFrontmatter } = await post.render()
postTitle={post.data.title}
postDescription={description}
postSlug={post.slug}
supportedLangs={supportedLangs}
>
<article class="heti mb-12.6">
<h1 class="post-title">

View file

@ -1,14 +1,12 @@
---
import PostList from '@/components/PostList.astro'
import { generateTagPaths } from '@/i18n/route'
import Layout from '@/layouts/Layout.astro'
import { getAllTags, getPostsByTag } from '@/utils/content'
export async function getStaticPaths() {
const tags = await getAllTags()
return tags.map(tag => ({
params: { tag },
props: { tag },
}))
return generateTagPaths(tags)
}
const { tag } = Astro.props

View file

@ -1,4 +1,4 @@
import type { langPath } from '@/utils/i18n/ui'
import type { langPath } from '@/i18n/ui'
type Exclude<T, U> = T extends U ? never : T

View file

@ -1,6 +1,6 @@
import type { CollectionEntry } from 'astro:content'
import themeConfig from '@/config'
import { langPath } from '@/utils/i18n/ui'
import { langPath } from '@/i18n/ui'
import { getCollection } from 'astro:content'
// Type definitions

View file

@ -1,89 +0,0 @@
import type { CollectionEntry } from 'astro:content'
import { themeConfig } from '@/config'
// 获取默认语言
const defaultLocale = themeConfig.global.locale
export function generateLanguagePaths() {
return themeConfig.global.moreLocale.map(lang => ({
params: { lang },
}))
}
export function generatePostPaths(posts: CollectionEntry<'posts'>[]) {
return posts.map(post => ({
params: {
slug: post.data.abbrlink || post.slug,
},
props: { post },
}))
}
export function generateMultiLangPostPaths(posts: CollectionEntry<'posts'>[]) {
interface PathResult {
params: {
lang: string
slug: string
}
props: {
post: CollectionEntry<'posts'>
}
}
const result: PathResult[] = []
posts.forEach((post) => {
// 确定这篇文章应该生成哪些语言版本
let postLangs: string[] = themeConfig.global.moreLocale
if (post.data.lang && typeof post.data.lang === 'string' && post.data.lang.trim() !== '') {
// 如果lang是单个字符串转为数组
postLangs = [post.data.lang]
}
// 处理非默认语言的路径
postLangs.forEach((lang) => {
// 跳过默认语言,它将通过 generatePostPaths 生成
if (lang !== defaultLocale) {
result.push({
params: {
lang,
slug: post.data.abbrlink || post.slug,
},
props: { post },
})
}
})
// 如果文章支持默认语言,则生成无语言代码的路径
// 默认语言条件未指定lang属性或lang属性等于defaultLocale
const supportsDefaultLang = !post.data.lang
|| (typeof post.data.lang === 'string' && post.data.lang === defaultLocale)
if (supportsDefaultLang) {
// 默认语言的路径不包含语言代码,在这里用特殊参数标记
// 这将由 [slug].astro 页面处理不在URL中显示语言代码
result.push({
params: {
lang: 'default',
slug: post.data.abbrlink || post.slug,
},
props: { post },
})
}
})
return result
}
export function generateMultiLangTagPaths(tags: string[]) {
return themeConfig.global.moreLocale.flatMap(lang =>
// 跳过默认语言,它将通过其他路径生成
lang !== defaultLocale
? tags.map(tag => ({
params: { lang, tag },
props: { tag },
}))
: [],
)
}