feat: implement ui i18n support
This commit is contained in:
parent
457fb93718
commit
698c411c71
13 changed files with 162 additions and 54 deletions
|
@ -1,10 +1,10 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import {defineConfig} from 'astro/config';
|
||||
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
import mdx from '@astrojs/mdx';
|
||||
|
||||
import { remarkWordCount } from './src/plugins/remark/wordcount.js';
|
||||
import {remarkWordCount} from './src/plugins/remark/wordcount.js';
|
||||
|
||||
import cloudflare from '@astrojs/cloudflare';
|
||||
import remarkMath from "remark-math";
|
||||
|
@ -31,8 +31,13 @@ export default defineConfig({
|
|||
theme: 'nord',
|
||||
wrap: true
|
||||
},
|
||||
remarkPlugins: [ remarkMath, remarkWordCount, remarkModifiedTime ],
|
||||
rehypePlugins: [ rehypeKatex ]
|
||||
remarkPlugins: [remarkMath, remarkWordCount, remarkModifiedTime],
|
||||
rehypePlugins: [rehypeKatex]
|
||||
},
|
||||
|
||||
i18n: {
|
||||
locales: ["en", "zh_hans"],
|
||||
defaultLocale: "en",
|
||||
},
|
||||
|
||||
integrations: [sitemap(), mdx(), partytown()],
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
---
|
||||
import {siteConfig} from "../config";
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
const noscript = siteConfig.noClientJavaScript
|
||||
---
|
||||
{noscript ? <a href="#top"><button id="toTopBtn" style="display: block" title="Go to top">Top</button></a> : <button id="toTopBtn" title="Go to top">Top</button>
|
||||
{noscript ? <a href="#top"><button id="toTopBtn" style="display: block" title={t('widget.back_to_top.title')}>{t('widget.back_to_top')}</button></a> : <button id="toTopBtn" title={t('widget.back_to_top.title')}>{t('widget.back_to_top')}</button>
|
||||
<script>
|
||||
// Get the button
|
||||
let toTopButton = document.getElementById("toTopBtn");
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
---
|
||||
const { listmonkInstance, listuuid } = Astro.props;
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
const { listmonkInstance, listuuid } = Astro.props;
|
||||
const inputId = Astro.props.listuuid.substring(0,5)
|
||||
---
|
||||
<details>
|
||||
<summary>Subscribe to newsletter</summary>
|
||||
<summary>{t('subscribe.newsletter')}}</summary>
|
||||
<small>{t('subscribe.newsletter.description')}</small>
|
||||
<form method="post" action={`https://${listmonkInstance}/subscription/form`} class="listmonk-form">
|
||||
<div>
|
||||
<input type="hidden" name="nonce" />
|
||||
|
||||
<input type="email" name="email" required placeholder="E-mail" />
|
||||
<input type="text" name="name" placeholder="Name (optional)" />
|
||||
<input type="email" name="email" required placeholder={t('subscribe.newsletter.email_label')} />
|
||||
<input type="text" name="name" placeholder={t('subscribe.newsletter.name_label')} />
|
||||
<input id={inputId} type="checkbox" name="l" checked="checked" value={listuuid} hidden />
|
||||
|
||||
<input type="submit" value="Subscribe" />
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
---
|
||||
import {siteConfig} from "../config";
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
const { title, email = siteConfig.defaultAuthor.email } = Astro.props;
|
||||
---
|
||||
<a href={`mailto:${email}?subject=RE:${title}&body=Hi,\n\nI would like to reply to your post "${title}".`}>↩ Reply via Email</a>
|
||||
<a href={`mailto:${email}?subject=RE:${title}&body=${t('article.reply_via_email.body')} "${title}".`}>↩ {t('article.reply_via_email')}</a>
|
|
@ -1,5 +1,10 @@
|
|||
---
|
||||
import {siteConfig} from "../config";
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
const noscript = siteConfig.noClientJavaScript
|
||||
const searchEngine = siteConfig.searchEngine || 'google'
|
||||
const domain = Astro.url.host
|
||||
|
@ -10,7 +15,7 @@ const domain = Astro.url.host
|
|||
"https://www.google.com/search"} method="GET" target="_blank">
|
||||
<div>
|
||||
<label for="search-input"><span class="command">search</span></label>
|
||||
<input name="q" type="text" id="search-input" class="search-input" autocomplete="off" placeholder="Type to search..." />
|
||||
<input name="q" type="text" id="search-input" class="search-input" autocomplete="off" placeholder={t('search.placeholder')} />
|
||||
{searchEngine === "duckduckgo" &&
|
||||
<input type="hidden" name="sites" value={domain} />
|
||||
}
|
||||
|
@ -32,7 +37,7 @@ const domain = Astro.url.host
|
|||
type="text"
|
||||
id="search-input"
|
||||
class="search-input"
|
||||
placeholder="Type to search..."
|
||||
placeholder={t('search.placeholder')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
---
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
|
||||
|
||||
interface Props {
|
||||
headings: MarkdownHeading[];
|
||||
|
@ -34,7 +40,7 @@ function buildHierarchy(headings: MarkdownHeading[]) {
|
|||
const toc = buildHierarchy(filteredHeadings);
|
||||
---
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
<summary>{t('article.toc')}</summary>
|
||||
<ul class="toc-list">
|
||||
{
|
||||
toc.map((heading) => (
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
---
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
---
|
||||
<div class="theme-dropdown" id="theme-dropdown">
|
||||
<button class="theme-switcher" id="theme-switcher">Theme</button>
|
||||
<button class="theme-switcher" id="theme-switcher">{t('widget.theme_mode')}</button>
|
||||
<div class="menu-body" id="menu-body">
|
||||
<div class="dropdown-item" data-theme="auto">System</div>
|
||||
<div class="dropdown-item" data-theme="dark">Dark</div>
|
||||
<div class="dropdown-item" data-theme="light">Light</div>
|
||||
<div class="dropdown-item" data-theme="auto">{t('widget.theme_mode.auto')}</div>
|
||||
<div class="dropdown-item" data-theme="dark">{t('widget.theme_mode.dark')}</div>
|
||||
<div class="dropdown-item" data-theme="light">{t('widget.theme_mode.light')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
---
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
---
|
||||
<div class="theme-switcher">
|
||||
<input type="checkbox" id="theme-toggle" class="theme-toggle" />
|
||||
<label for="theme-toggle" class="theme-label">
|
||||
<span>Theme</span>
|
||||
<span>{t('widget.theme_mode')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<style>
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { getMetadata, getWaybackMetadata } from '../../plugins/get-metadata';
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
|
@ -67,8 +72,8 @@ const archiveUrl = showArchive ? await getWaybackMetadata(url, timestamp) : null
|
|||
|
||||
{showArchive && archiveUrl && (
|
||||
<div class="link-card__archive">
|
||||
<a href={archiveUrl} target="_blank" rel="noopener noreferrer" title="View archived version">
|
||||
View archived version
|
||||
<a href={archiveUrl} target="_blank" rel="noopener noreferrer" title={t('component.link_card.view_archived_version')}>
|
||||
{t('component.link_card.view_archived_version')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
---
|
||||
import { encrypt } from '../../plugins/encrypt';
|
||||
import {siteConfig} from "../../config";
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
|
||||
interface Props {
|
||||
password?: string;
|
||||
|
@ -20,10 +25,10 @@ const { encryptedData, iv } = encrypt(content, password);
|
|||
|
||||
<div class="encrypted-content" data-encrypted={encryptedData} data-iv={iv}>
|
||||
<div class="password-form">
|
||||
<p>This content is protected. Enter the password to view it:</p>
|
||||
<p>{t('component.protected_content.tip')}</p>
|
||||
<div>
|
||||
<input type="password" class="decrypt-password" title="password" />
|
||||
<button class="decrypt-button">Decrypt</button>
|
||||
<button class="decrypt-button">{t('component.protected_content.button_label')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-container hidden"></div>
|
||||
|
|
|
@ -1,19 +1,65 @@
|
|||
import { i18n } from "astro:config/client"
|
||||
|
||||
export const languages = {
|
||||
en: 'English',
|
||||
zh_hans: '中文(简体)',
|
||||
};
|
||||
|
||||
export const defaultLang = 'en';
|
||||
export const defaultLang: string = i18n?.defaultLocale || 'en';
|
||||
export const showDefaultLang = false;
|
||||
|
||||
export const ui = {
|
||||
en: {
|
||||
'nav.home': 'Home',
|
||||
'nav.about': 'About',
|
||||
'nav.twitter': 'Twitter',
|
||||
'nav.blog': 'Blog',
|
||||
'search.placeholder': 'Search posts...',
|
||||
'posts.description': 'Posts from the terminal.',
|
||||
'subscribe.rss': 'Subscribe to RSS feed',
|
||||
'subscribe.newsletter': 'Subscribe to Newsletter',
|
||||
'subscribe.newsletter.description': 'Get the latest posts delivered right to your inbox.',
|
||||
'subscribe.newsletter.email_label': 'E-mail',
|
||||
'subscribe.newsletter.name_label': 'Name (optional)',
|
||||
'widget.back_to_top': 'Top',
|
||||
'widget.back_to_top.title': 'Back to top',
|
||||
'widget.theme_mode': 'Theme',
|
||||
'widget.theme_mode.light': 'Light',
|
||||
'widget.theme_mode.dark': 'Dark',
|
||||
'widget.theme_mode.auto': 'System',
|
||||
'article.last_update': 'Updated on',
|
||||
'article.word_count': 'words',
|
||||
'article.toc': 'Table of Contents',
|
||||
'article.reply_via_email': 'Reply via Email',
|
||||
'article.reply_via_email.body': 'Hi,I would like to reply to your post',
|
||||
'article.back_to_posts': 'Back to posts',
|
||||
'article.comments': 'Comments',
|
||||
'component.protected_content.tip': 'This content is protected. Enter the password to view it:',
|
||||
'component.protected_content.button_label': 'Decrypt',
|
||||
'component.link_card.view_archived_version': 'View archived version',
|
||||
},
|
||||
zh_hans: {
|
||||
'nav.home': '首页',
|
||||
'nav.about': '关于',
|
||||
'nav.blog': '博客',
|
||||
'search.placeholder': '搜索文章',
|
||||
'subscribe.rss': '订阅 RSS',
|
||||
'subscribe.newsletter': '订阅 Newsletter',
|
||||
'subscribe.newsletter.description': '新文章第一时间送达您的邮箱。',
|
||||
'subscribe.newsletter.email_label': '邮箱',
|
||||
'subscribe.newsletter.name_label': '名称(可选)',
|
||||
'widget.back_to_top': '回顶',
|
||||
'widget.back_to_top.title': '回到顶部',
|
||||
'widget.theme_mode': '主题',
|
||||
'widget.theme_mode.light': '浅色主题',
|
||||
'widget.theme_mode.dark': '深色主题',
|
||||
'widget.theme_mode.auto': '跟随系统',
|
||||
'article.last_update': '编辑于',
|
||||
'article.word_count': '字',
|
||||
'article.toc': '文章目录',
|
||||
'article.reply_via_email': '邮件回复',
|
||||
'article.reply_via_email.body': '您好,我想回复您的文章:',
|
||||
'article.back_to_posts': '返回文章列表',
|
||||
'article.comments': '评论区',
|
||||
'component.protected_content.tip': '此内容已被加密,请输入密码以查看:',
|
||||
'component.protected_content.button_label': '确定',
|
||||
'component.link_card.view_archived_version': '查看互联网档案馆的存档版本',
|
||||
},
|
||||
} as const;
|
|
@ -4,16 +4,20 @@ import { getCollection } from 'astro:content';
|
|||
import NewsLetter from "../components/NewsLetter.astro";
|
||||
import {siteConfig} from "../config";
|
||||
import ArticleList from "../components/ArticleList.astro";
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
const posts = await getCollection('posts');
|
||||
posts.sort((a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime());
|
||||
---
|
||||
|
||||
<Layout title="Blog Posts" description="List all files and folders in the directory.">
|
||||
<Layout title="Blog Posts" description="List all posts on the website.">
|
||||
<h1 class="title">~/blog</h1>
|
||||
|
||||
<div class="content">
|
||||
<p class="typewriter">Posts from the terminal.</p>
|
||||
<p class="typewriter">{t('posts.description')}</p>
|
||||
|
||||
<div style="margin-top: 2rem;">
|
||||
<span class="command">ls -la posts/</span>
|
||||
|
@ -22,9 +26,9 @@ posts.sort((a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDat
|
|||
|
||||
<div style="margin-top: 2rem;">
|
||||
<p>
|
||||
<span class="command">cat rss.txt</span>
|
||||
<span class="command">cat subscribe.txt</span>
|
||||
<br />
|
||||
<a href="/rss.xml" style="margin-left: 1rem;">Subscribe to RSS feed</a>
|
||||
<a href="/rss.xml" style="margin-left: 1rem;">{t('subscribe.rss')}</a>
|
||||
</p>
|
||||
{siteConfig.newsletter.enabled && <NewsLetter listmonkInstance={siteConfig.newsletter.listmonk.instanceDomain} listuuid={siteConfig.newsletter.listmonk.listuuid} />}
|
||||
</div>
|
||||
|
|
|
@ -10,6 +10,11 @@ import { ExtractFirstImage } from '../../plugins/extract-images';
|
|||
import AuthorInfo from "../../components/helper/authors/Info.astro";
|
||||
import TableOfContents from "../../components/TableOfContents.astro";
|
||||
import "katex/dist/katex.css"
|
||||
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../../i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const translatePath = useTranslatedPath(lang);
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const blogEntries = await getCollection('posts');
|
||||
|
@ -29,6 +34,9 @@ const author = Array.isArray(entry.data.author) ? entry.data.author : (entry.dat
|
|||
const wordcount = remarkPluginFrontmatter.wordcount;
|
||||
const lastUpdated = remarkPluginFrontmatter.lastModified;
|
||||
|
||||
const pubDate = new Date(entry.data.pubDate).toISOString().split('T')[0]
|
||||
const lastUpdatedDate = new Date(lastUpdated).toISOString().split('T')[0]
|
||||
|
||||
// Get author data
|
||||
const authorData = await Promise.all((author).map((singleAuthor) => getEntry(singleAuthor).then(authorEntry => authorEntry?.data)))
|
||||
const authorInfo = authorData.includes(undefined) ? [{data: siteConfig.defaultAuthor}] : authorData;
|
||||
|
@ -56,10 +64,10 @@ const cover = customFeaturedImage || matchedImage_src?.src || firstImageURL || `
|
|||
<article>
|
||||
<h1 class="title">{entry.data.title}</h1>
|
||||
{authorInfo.map((a: any) => <AuthorInfo data={a} />)}
|
||||
<span class="date">{new Date(entry.data.pubDate).toISOString().split('T')[0]}</span>
|
||||
<span class="date">(Updated on {new Date(lastUpdated).toISOString().split('T')[0]})</span>
|
||||
<span class="date">{pubDate}</span>
|
||||
{pubDate === lastUpdatedDate && <span class="date">({t('article.last_update')} {lastUpdatedDate})</span>}
|
||||
<span>|</span>
|
||||
<span class="wordcount">{wordcount.words} words</span>
|
||||
<span class="wordcount">{wordcount.words} {t('article.word_count')}</span>
|
||||
{ (cover && cover !== firstImageURL && cover !== `/blog/${slug}/featured.png`) && <Image class="cover" width=720 height=480 src={cover} alt={`cover of ${entry.data.title}`} /> }
|
||||
{headings.length !== 0 && <TableOfContents headings={headings} />}
|
||||
{entry.data.summary && <p class="summary">{entry.data.summary}</p> }
|
||||
|
@ -70,8 +78,8 @@ const cover = customFeaturedImage || matchedImage_src?.src || firstImageURL || `
|
|||
<div class="extra-post" style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
||||
<ReplyViaEmail title={entry.data.title} email={authorInfo[0].email} />
|
||||
<br>
|
||||
<a href="/blog">← Back to posts</a>
|
||||
{!noscript && <h2>Comments</h2> <Comments />}
|
||||
<a href="/blog">← {t('article.back_to_posts')}</a>
|
||||
{!noscript && <h2>{t('article.comments')}</h2> <Comments />}
|
||||
{!noscript &&
|
||||
<script>
|
||||
import "katex/dist/contrib/copy-tex.js"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue