mirror of
https://github.com/reonokiy/blog.nokiy.net.git
synced 2025-06-15 11:12:54 +02:00
feat: implement internationalization (i18n) support
This commit is contained in:
parent
32ffec8480
commit
d6c98880d3
17 changed files with 247 additions and 18 deletions
|
@ -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: {
|
||||
|
|
11
public/image/astro-icon.svg
Normal file
11
public/image/astro-icon.svg
Normal 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 |
|
@ -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 -->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import Head from '@/components/Head.astro'
|
||||
---
|
||||
|
||||
<html lang="zh-CN">
|
||||
<html lang={Astro.currentLocale || 'en-US'}>
|
||||
<head>
|
||||
<Head />
|
||||
</head>
|
||||
|
|
7
src/pages/[lang]/about.astro
Normal file
7
src/pages/[lang]/about.astro
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
---
|
||||
|
||||
<Layout>
|
||||
about me
|
||||
</Layout>
|
47
src/pages/[lang]/index.astro
Normal file
47
src/pages/[lang]/index.astro
Normal 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>
|
23
src/pages/[lang]/posts/[slug].astro
Normal file
23
src/pages/[lang]/posts/[slug].astro
Normal 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>
|
36
src/pages/[lang]/tags/[tags].astro
Normal file
36
src/pages/[lang]/tags/[tags].astro
Normal 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>
|
15
src/pages/[lang]/tags/index.astro
Normal file
15
src/pages/[lang]/tags/index.astro
Normal 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
7
src/pages/about.astro
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
---
|
||||
|
||||
<Layout>
|
||||
about me
|
||||
</Layout>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
36
src/pages/tags/[tags].astro
Normal file
36
src/pages/tags/[tags].astro
Normal 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>
|
15
src/pages/tags/index.astro
Normal file
15
src/pages/tags/index.astro
Normal 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>
|
5
src/types/index.d.ts
vendored
5
src/types/index.d.ts
vendored
|
@ -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
|
||||
|
|
|
@ -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: 'О себе',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue