feat: implement internationalization (i18n) support

This commit is contained in:
radishzzz 2025-01-18 00:01:25 +00:00
parent 32ffec8480
commit d6c98880d3
17 changed files with 247 additions and 18 deletions

View file

@ -28,13 +28,22 @@ import { GithubCardComponent } from './src/plugins/rehype-component-github-card.
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.mjs'
import { langMap } from './src/utils/ui'
const { url }: { url: ThemeConfig['site']['url'] } = themeConfig.site
const { locale }: { locale: ThemeConfig['global']['locale'] } = themeConfig.global
export default defineConfig({
site: url,
base: '/',
trailingSlash: 'always',
i18n: {
locales: Object.entries(langMap).map(([path, codes]) => ({
path,
codes,
})),
defaultLocale: locale,
},
integrations: [
partytown({
config: {

View file

@ -0,0 +1,11 @@
<svg width="85" height="107" viewBox="0 0 85 107" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.5894 91.1365C22.7555 86.7178 21.3444 77.4335 23.3583 70.7072C26.8503 74.948 31.6888 76.2914 36.7005 77.0497C44.4375 78.2199 52.0359 77.7822 59.2232 74.2459C60.0454 73.841 60.8052 73.3027 61.7036 72.7574C62.378 74.714 62.5535 76.6892 62.318 78.6996C61.7452 83.5957 59.3086 87.3778 55.4332 90.2448C53.8835 91.3916 52.2437 92.4167 50.6432 93.4979C45.7262 96.8213 44.3959 100.718 46.2435 106.386C46.2874 106.525 46.3267 106.663 46.426 107C43.9155 105.876 42.0817 104.24 40.6845 102.089C39.2087 99.8193 38.5066 97.3081 38.4696 94.5909C38.4511 93.2686 38.4511 91.9345 38.2733 90.6309C37.8391 87.4527 36.3471 86.0297 33.5364 85.9478C30.6518 85.8636 28.37 87.6469 27.7649 90.4554C27.7187 90.6707 27.6517 90.8837 27.5847 91.1341L27.5894 91.1365Z" fill="white"/>
<path d="M27.5894 91.1365C22.7555 86.7178 21.3444 77.4335 23.3583 70.7072C26.8503 74.948 31.6888 76.2914 36.7005 77.0497C44.4375 78.2199 52.0359 77.7822 59.2232 74.2459C60.0454 73.841 60.8052 73.3027 61.7036 72.7574C62.378 74.714 62.5535 76.6892 62.318 78.6996C61.7452 83.5957 59.3086 87.3778 55.4332 90.2448C53.8835 91.3916 52.2437 92.4167 50.6432 93.4979C45.7262 96.8213 44.3959 100.718 46.2435 106.386C46.2874 106.525 46.3267 106.663 46.426 107C43.9155 105.876 42.0817 104.24 40.6845 102.089C39.2087 99.8193 38.5066 97.3081 38.4696 94.5909C38.4511 93.2686 38.4511 91.9345 38.2733 90.6309C37.8391 87.4527 36.3471 86.0297 33.5364 85.9478C30.6518 85.8636 28.37 87.6469 27.7649 90.4554C27.7187 90.6707 27.6517 90.8837 27.5847 91.1341L27.5894 91.1365Z" fill="url(#paint0_linear_1_59)"/>
<path d="M0 69.5866C0 69.5866 14.3139 62.6137 28.6678 62.6137L39.4901 29.1204C39.8953 27.5007 41.0783 26.3999 42.4139 26.3999C43.7495 26.3999 44.9325 27.5007 45.3377 29.1204L56.1601 62.6137C73.1601 62.6137 84.8278 69.5866 84.8278 69.5866C84.8278 69.5866 60.5145 3.35233 60.467 3.21944C59.7692 1.2612 58.5911 0 57.0029 0H27.8274C26.2392 0 25.1087 1.2612 24.3634 3.21944C24.3108 3.34983 0 69.5866 0 69.5866Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_1_59" x1="22.4702" y1="107" x2="69.1451" y2="84.9468" gradientUnits="userSpaceOnUse">
<stop stop-color="#D83333"/>
<stop offset="1" stop-color="#F041FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -10,7 +10,7 @@ interface Props {
const { postTitle, postDescription, postImage } = Astro.props
const { title, subtitle, description, author, url, favicon } = themeConfig.site
const { light: { backgroundStart: lightMode }, dark: { backgroundStart: darkMode } } = themeConfig.color
const { language } = themeConfig.global
const { locale, moreLocale } = themeConfig.global
const { verification = {}, twitterID = '', facebookID = '', facebookLink = '', googleAnalyticsID = '', umamiAnalyticsID = '', siteScreenshot = '' } = themeConfig.seo ?? {}
const { google = '', bing = '', yandex = '', baidu = '' } = verification
const { cdn, commentURL = '', imageHostURL = '', customGoogleAnalyticsURL = '', customUmamiAnalyticsURL = '', customUmamiAnalyticsJS = '' } = themeConfig.preload
@ -46,11 +46,18 @@ const { cdn, commentURL = '', imageHostURL = '', customGoogleAnalyticsURL = '',
<link rel="author" href={url} />
<link rel="publisher" href={author} />
<link rel="canonical" href={Astro.url} />
<!-- todo language -->
<link rel="alternate" href="/en/" hreflang="en" />
<link rel="alternate" href="/rss.xml" type="application/rss+xml" title="RSS" />
<link rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/" />
<!-- i18n hreflang generate -->
{[locale, ...moreLocale].map(lang => (
<link
rel="alternate"
href={lang === locale ? '/' : `/${lang}/`}
hreflang={lang === 'zh-tw' ? 'zh-TW' : lang}
/>
))}
<!-- Facebook Open Graph -->
<meta property="fb:app_id" content={facebookID} />
<meta property="og:url" content={Astro.url} />
@ -60,7 +67,7 @@ const { cdn, commentURL = '', imageHostURL = '', customGoogleAnalyticsURL = '',
<meta property="og:image:alt" content={postTitle || title} />
<meta property="og:description" content={postDescription || subtitle} />
<meta property="og:site_name" content={title} />
<meta property="og:locale" content={language} />
<meta property="og:locale" content={Astro.currentLocale?.replace('-', '_') || 'en_US'} />
<meta property="article:author" content={facebookLink} />
<!-- Twitter Card -->

View file

@ -30,7 +30,8 @@ export const themeConfig: ThemeConfig = {
// GLOBAL SETTINGS >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> START
global: {
language: 'zh-CN', // en-US, zh-CN
locale: 'zh', // zh, zh-tw, ja, en, es, ru, default locale setting
moreLocale: ['zh-tw', 'ja', 'en', 'es', 'ru'], // ['zh-tw', 'ja', 'en', 'es', 'ru'] not fill in the default locale code again
font: 'sans', // sans, serif, choose the font style for posts
rss: true, // true, false, whether to enable RSS
toc: true, // true, false, whether to enable table of contents in posts

View file

@ -2,7 +2,7 @@
import Head from '@/components/Head.astro'
---
<html lang="zh-CN">
<html lang={Astro.currentLocale || 'en-US'}>
<head>
<Head />
</head>

View file

@ -0,0 +1,7 @@
---
import Layout from '@/layouts/Layout.astro'
---
<Layout>
about me
</Layout>

View file

@ -0,0 +1,47 @@
---
import Layout from '@/layouts/Layout.astro'
import { getPinnedPosts, getPosts } from '@/utils/content.config'
export function getStaticPaths() {
return [
{ params: { lang: 'zh' } },
{ params: { lang: 'en' } },
]
}
const { lang } = Astro.params
const pinnedPosts = await getPinnedPosts()
const posts = await getPosts()
---
<Layout>
<main>
{pinnedPosts.length > 0 && (
<section>
<ul>
{pinnedPosts.map(post => (
<li>
<a href={`/${lang}/posts/${post.slug}/`}>
{post.data.title}
<time>({post.data.published.toISOString().split('T')[0]})</time>
</a>
</li>
))}
</ul>
</section>
)}
<section>
<ul>
{posts.map(post => (
<li>
<a href={`/${lang}/posts/${post.slug}/`}>
{post.data.title}
<time>({post.data.published.toISOString().split('T')[0]})</time>
</a>
</li>
))}
</ul>
</section>
</main>
</Layout>

View file

@ -0,0 +1,23 @@
---
import Layout from '@/layouts/Layout.astro'
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const posts = await getCollection('posts')
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}))
}
const { post } = Astro.props
const { Content } = await post.render()
---
<Layout>
<article>
<h1>{post.data.title}</h1>
<time>{post.data.published.toISOString().split('T')[0]}</time>
<Content />
</article>
</Layout>

View file

@ -0,0 +1,36 @@
---
import Layout from '@/layouts/Layout.astro'
import { getAllTags, getPostsByTag } from '@/utils/content.config'
export async function getStaticPaths() {
const tags = await getAllTags()
return tags.map(tag => ({
params: { tags: tag },
props: { tags: tag },
}))
}
const { tags } = Astro.props
const posts = await getPostsByTag(tags)
const allTags = await getAllTags()
---
<Layout>
<div>
{allTags.map(tag => (
<a href={`/tags/${tag}/`}>
{tag}
</a>
))}
</div>
<div>
<ul>
{posts.map(post => (
<li>
<a href={`/posts/${post.slug}/`}>{post.data.title}</a>
</li>
))}
</ul>
</div>
</Layout>

View file

@ -0,0 +1,15 @@
---
import Layout from '@/layouts/Layout.astro'
import { getAllTags } from '@/utils/content.config'
const allTags = await getAllTags()
---
<Layout>
<div>
{allTags.map(tag => (
<a href={`/tags/${tag}/`}>
{tag}
</a>
))}
</div>

7
src/pages/about.astro Normal file
View file

@ -0,0 +1,7 @@
---
import Layout from '@/layouts/Layout.astro'
---
<Layout>
about me
</Layout>

View file

@ -10,7 +10,6 @@ const pinnedPosts = await getPinnedPosts()
<main>
{pinnedPosts.length > 0 && (
<section>
<h2>置顶文章</h2>
<ul>
{pinnedPosts.map(post => (
<li>
@ -25,7 +24,6 @@ const pinnedPosts = await getPinnedPosts()
)}
<section>
<h2>所有文章</h2>
<ul>
{posts.map(post => (
<li>

View file

@ -8,7 +8,7 @@ import sanitizeHtml from 'sanitize-html'
const parser = new MarkdownIt()
const { title, description, url } = themeConfig.site
const { language } = themeConfig.global
const { locale } = themeConfig.global
const followConfig = themeConfig.seo?.follow
// Extract first 100 chars from content as description
@ -49,7 +49,7 @@ export async function GET(_context: APIContext) {
),
})),
customData: `
<language>${language}</language>
<language>${locale}</language>
${followConfig?.feedID && followConfig?.userID
? `<follow_challenge>
<feedId>${followConfig.feedID}</feedId>

View file

@ -0,0 +1,36 @@
---
import Layout from '@/layouts/Layout.astro'
import { getAllTags, getPostsByTag } from '@/utils/content.config'
export async function getStaticPaths() {
const tags = await getAllTags()
return tags.map(tag => ({
params: { tags: tag },
props: { tags: tag },
}))
}
const { tags } = Astro.props
const posts = await getPostsByTag(tags)
const allTags = await getAllTags()
---
<Layout>
<div>
{allTags.map(tag => (
<a href={`/tags/${tag}/`}>
{tag}
</a>
))}
</div>
<div>
<ul>
{posts.map(post => (
<li>
<a href={`/posts/${post.slug}/`}>{post.data.title}</a>
</li>
))}
</ul>
</div>
</Layout>

View file

@ -0,0 +1,15 @@
---
import Layout from '@/layouts/Layout.astro'
import { getAllTags } from '@/utils/content.config'
const allTags = await getAllTags()
---
<Layout>
<div>
{allTags.map(tag => (
<a href={`/tags/${tag}/`}>
{tag}
</a>
))}
</div>

View file

@ -1,3 +1,5 @@
import type { langPath } from '@/utils/ui'
export interface ThemeConfig {
site: {
@ -24,7 +26,8 @@ export interface ThemeConfig {
}
global: {
language: string
locale: typeof langPath[number]
moreLocale: typeof langPath[number][]
font: string
rss: boolean
toc: boolean

View file

@ -1,30 +1,44 @@
export const language = {
zh: {
// Global Language Map
export const langMap: Record<string, string[]> = {
'zh': ['zh-CN'],
'zh-tw': ['zh-TW'],
'ja': ['ja-JP'],
'en': ['en-US'],
'es': ['es-ES'],
'ru': ['ru-RU'],
}
// Standard Language Code
export const langCode = Object.values(langMap).flat()
// Abbreviated Language Code
export const langPath = Object.keys(langMap).flat()
// UI Translation
export const ui = {
'zh': {
posts: '文章',
tags: '标签',
about: '关于',
},
tw: {
'zh-tw': {
posts: '文章',
tags: '標籤',
about: '關於',
},
ja: {
'ja': {
posts: '記事',
tags: 'タグ',
about: '概要',
},
en: {
'en': {
posts: 'Posts',
tags: 'Tags',
about: 'About',
},
es: {
'es': {
posts: 'Posts',
tags: 'Tags',
about: 'Sobre',
},
ru: {
'ru': {
posts: 'Посты',
tags: 'Теги',
about: 'О себе',