Skip to content

feat(i18n): 全站接入 next-intl,删除 lib/i18n 自建系统#287

Merged
longsizhuo merged 3 commits intomainfrom
feat/i18n-global-ui
Apr 16, 2026
Merged

feat(i18n): 全站接入 next-intl,删除 lib/i18n 自建系统#287
longsizhuo merged 3 commits intomainfrom
feat/i18n-global-ui

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

@longsizhuo longsizhuo commented Apr 16, 2026

Summary

  • 接入 next-intl 4.x(App Router 原生),完整替换原有 lib/i18n 自建方案
  • 新建 i18n/request.ts(读 locale cookie → 选 zh/en messages)+ messages/zh.json + messages/en.json,覆盖 16+ 命名空间
  • 29 个组件从 getServerT()/useT() 迁至 getTranslations()/useTranslations(),范围涵盖 Header/Hero/Footer、Settings、Login、NotFound、HotDocsPreview、PageFeedback、UserMenu、DocShareButton、DocHistoryPanel、EditorMetadataForm、Contribute、以及全部 Profile(page.tsx/ActivityHeatmap/ProfileCard/EditLinkIfOwner/FollowButton/GithubRepos/edit)
  • 删除 lib/i18n/client.tsxmessages.tsserver.ts;layout.tsx 移除 LocaleProvider
  • typecheck 零报错,pre-commit hooks 全通过

Test plan

  • 中文 cookie 下首页/profile 等页面文案正确显示中文
  • 切换 locale cookie 为 en 后重新加载,文案切换为英文
  • /u/{username} 热力图月份标签随 locale 正确渲染(zh: "1月",en: "Jan")
  • 设置页保存 toast、编辑页表单 label 全部本地化
  • 无控制台报错(next-intl missing key 警告为零)

- 新增 i18n/request.ts(getRequestConfig,读 locale cookie)
- 新增 messages/zh.json + messages/en.json(16+ 命名空间)
- next.config.mjs 切换到 withNextIntl("./i18n/request.ts")
- layout.tsx 换 NextIntlClientProvider,移除 LocaleProvider
- P0: Header/Hero/Footer → getTranslations
- P1: SettingsForm/LoginPage/NotFound → getTranslations/useTranslations
- P2: HotDocsPreview/PageFeedback/UserMenu/DocShareButton/DocHistoryPanel/EditorMetadataForm/Contribute
- Profile: page.tsx/ActivityHeatmap/ProfileCard/EditLinkIfOwner/FollowButton/GithubRepos/edit/page.tsx/EditProfileForm → next-intl
- 删除 lib/i18n/client.tsx、messages.ts、server.ts
Copilot AI review requested due to automatic review settings April 16, 2026 18:44
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
involutionhell-github-io Ready Ready Preview, Comment Apr 16, 2026 7:36pm
website-preview Ready Ready Preview, Comment Apr 16, 2026 7:36pm

profile 下的 follow/activity/card/repos/docs/edit 统一从独立顶层 namespace
改为 profile.xxx 嵌套路径,与 translator 发布的新字典结构一致。
pageFeedback → feedback namespace,sec3/sec4 titleField key 修正。
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

此 PR 将站点从自建 lib/i18n 翻译方案迁移到 next-intl(App Router 原生方案),并引入 JSON messages 与 request 级别的 locale/messages 解析。

Changes:

  • 接入 next-intl(createNextIntlPlugin + i18n/request.ts + NextIntlClientProvider),移除旧的 lib/i18n/* 实现
  • 新增中英文 messages(messages/zh.jsonmessages/en.json)并将多个页面/组件迁移到 getTranslations() / useTranslations()
  • 对 settings/profile/editor/feedback 等 UI 文案进行本地化替换

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
next.config.mjs next-intl 插件配置指向新的 request config 文件
messages/zh.json 新增中文 messages(多命名空间)
messages/en.json 新增英文 messages(多命名空间)
lib/i18n/server.ts 删除旧 i18n 服务端工具
lib/i18n/messages.ts 删除旧 i18n 字典与格式化逻辑
lib/i18n/client.tsx 删除旧 i18n 客户端 Provider/hook
i18n/request.ts 新增 next-intl request 级别 locale/messages 装载逻辑
app/u/[username]/page.tsx Profile 页面改用 next-intl server translations
app/u/[username]/edit/page.tsx Profile 编辑页改用 next-intl server translations
app/u/[username]/edit/EditProfileForm.tsx Profile 编辑表单改用 next-intl client translations
app/u/[username]/ProfileCard.tsx ProfileCard 改用 next-intl client translations
app/u/[username]/GithubRepos.tsx GitHub repos 区块改用 next-intl server translations
app/u/[username]/FollowButton.tsx FollowButton 改用 next-intl client translations
app/u/[username]/EditLinkIfOwner.tsx 编辑入口文案改用 next-intl client translations
app/u/[username]/ActivityHeatmap.tsx 活跃热力图改用 next-intl server translations
app/settings/SettingsForm.tsx Settings toast/label 改用 next-intl client translations
app/not-found.tsx 404 页面文案改用 next-intl
app/login/page.tsx 登录页文案改用 next-intl
app/layout.tsx 用 NextIntlClientProvider 替换旧 LocaleProvider,并注入 messages
app/components/UserMenu.tsx 用户菜单文案改用 next-intl
app/components/PageFeedback.tsx 反馈组件文案改用 next-intl
app/components/HotDocsPreview.tsx 热门文档预览引入 next-intl 翻译
app/components/Hero.tsx Hero 组件改为 async 并引入 next-intl 翻译
app/components/Header.tsx Header 改为 async 并引入 next-intl 翻译
app/components/Footer.tsx Footer 改为 async 并引入 next-intl 翻译
app/components/EditorMetadataForm.tsx 编辑器元数据表单文案改用 next-intl
app/components/DocShareButton.tsx 分享按钮文案改用 next-intl
app/components/DocHistoryPanel.tsx 历史面板文案/相对时间改用 next-intl
app/components/Contribute.tsx 投稿组件文案改用 next-intl
Comments suppressed due to low confidence (2)

app/u/[username]/edit/EditProfileForm.tsx:112

  • 这里使用 useTranslations("edit"),但 messages/* 中对应 key 实际在 profile.edit.* 下。按当前结构会导致表单内所有文案 missing key。建议将 namespace 改为 profile.edit(或同步把消息结构改成顶层 edit)。
export function EditProfileForm({ targetIdentifier }: Props) {
  const { user, status } = useAuth();
  const router = useRouter();
  const t = useTranslations("profile.edit");

  const [prefs, setPrefs] = useState<Preferences>(EMPTY_PREFS);
  const [loading, setLoading] = useState(true);
  const [saving, setSaving] = useState(false);

app/components/HotDocsPreview.tsx:56

  • 这里仍然硬编码了英文标题/按钮文案(Hot This WeekMORE),导致在中文 locale 下也会显示英文,且与下方 {t("subtitle")} 产生重复/混搭。PR 描述与测试计划强调按 locale 切换文案,建议将这些硬编码文本也改为使用 hotDocs 命名空间(例如 subtitle/more),或调整布局避免同一含义渲染两次。
        <div>
          <div className="font-serif text-lg font-black uppercase text-[var(--foreground)]">
            Hot This Week
          </div>
          <div className="font-mono text-[10px] uppercase tracking-widest text-neutral-500">
            {t("subtitle")}
          </div>
        </div>
        <Link
          href="/rank?tab=hot&window=7d"
          className="font-mono text-[10px] uppercase tracking-widest font-bold text-[var(--foreground)] hover:text-[#CC0000] transition-colors flex items-center gap-1 group"
          data-umami-event="navigation_click"
          data-umami-event-region="hot_docs_preview"
          data-umami-event-label="MORE"
        >
          MORE
          <span className="transform group-hover:translate-x-0.5 transition-transform">

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 15 to 18
export default async function EditProfilePage({ params }: Param) {
const { username } = await params;
const t = await getServerT();
const t = await getTranslations("profile.edit");
return (
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里使用 getTranslations("edit"),但 messages/* 中编辑页文案位于 profile.edit.*edit 不是顶层 namespace)。这会触发 missing key。建议把 namespace 改为 profile.edit(或调整消息文件结构把 edit 提升到顶层)。

Copilot uses AI. Check for mistakes.
Comment on lines 38 to 44
*/
export function FollowButton({ identifier, targetUserId }: Props) {
const { user, status } = useAuth();
const t = useT();
const t = useTranslations("profile.follow");
const [followerCount, setFollowerCount] = useState<number | null>(null);
const [followingCount, setFollowingCount] = useState<number | null>(null);
const [isFollowing, setIsFollowing] = useState(false);
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里使用 useTranslations("follow"),但 messages/* 中 follow 文案位于 profile.follow.*(非顶层 follow)。按当前写法会导致按钮/统计文案 missing key。建议将 namespace 改为 profile.follow(或把消息文件调整为顶层 follow)。

Copilot uses AI. Check for mistakes.
Comment on lines 9 to 12
export function PageFeedback() {
const pathname = usePathname();
const t = useTranslations("feedback");
const [voted, setVoted] = useState<"helpful" | "not_helpful" | null>(null);
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useTranslations("pageFeedback") 对应的命名空间在 messages/zh.json/en.json 中不存在(当前是 feedback.question / feedback.thanks)。这会触发 next-intl missing key。建议将 namespace 改为 feedback(或同步新增 pageFeedback 命名空间)。

Copilot uses AI. Check for mistakes.
Comment thread app/u/[username]/page.tsx
Comment on lines 231 to +233

const t = await getServerT();
const t = await getTranslations("profile");
const tDocs = await getTranslations("profile.docs");
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tDocs 通过 getTranslations("docs") 获取,但 messages/*docs 并不是顶层命名空间,而是在 profile.docs 下(见 messages/zh.jsonprofile.docs.*)。这会导致 docs 区块翻译 missing key。建议改为 getTranslations("profile.docs")(或复用 getTranslations("profile") 并用 t("docs.*"))。

Copilot uses AI. Check for mistakes.
Comment on lines 56 to 60
export async function GithubRepos({ identifier }: Props) {
const repos = await fetchRepos(identifier);
if (repos.length === 0) return null;
const t = await getServerT();
const t = await getTranslations("profile.repos");

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里使用 getTranslations("repos"),但 messages/* 中 repos 文案位于 profile.repos.*(非顶层 repos)。当前会触发 missing key。建议改为 getTranslations("profile.repos") 或改消息文件结构与代码对齐。

Copilot uses AI. Check for mistakes.
Comment thread app/layout.tsx
Comment on lines 215 to +220
{/*
LocaleProvider 把服务端读出的 locale 注入客户端 Context,
客户端组件通过 useT() 拿到翻译函数,保持 SSR/CSR 一致,
不在客户端重新读 cookie 避免水合抖动。
*/}
<LocaleProvider locale={locale}>
<NextIntlClientProvider locale={locale} messages={messages}>
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RootLayout 里这段注释仍在描述已删除的 LocaleProvider / useT()(现在改成了 NextIntlClientProvider + useTranslations)。建议更新注释以匹配当前实现,避免后续维护者误解 i18n 注入链路。

Copilot uses AI. Check for mistakes.
Comment thread app/layout.tsx Outdated
Comment on lines 136 to 139
const searchApi = `/search.${locale}.json`;
const messages = await getMessages();
return (
<html lang="zh-CN" suppressHydrationWarning>
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前 <html lang="zh-CN"> 是硬编码的,但这里已经根据 cookie/next-intl 设置了 locale 并注入 NextIntlClientProvider。为无障碍与 SEO(屏幕阅读器语言、拼写检查等),建议让 lang 随 locale 切换(例如 zh 用 zh-CN,en 用 en/en-US),并避免出现 provider locale 与 lang 不一致的情况。

Suggested change
const searchApi = `/search.${locale}.json`;
const messages = await getMessages();
return (
<html lang="zh-CN" suppressHydrationWarning>
const htmlLang = locale === "en" ? "en" : "zh-CN";
const searchApi = `/search.${locale}.json`;
const messages = await getMessages();
return (
<html lang={htmlLang} suppressHydrationWarning>

Copilot uses AI. Check for mistakes.
Comment thread app/components/Footer.tsx Outdated
Comment on lines +22 to +45
<p className="font-body text-neutral-600 dark:text-neutral-400 mb-8 max-w-md text-justify leading-relaxed">
一个由开发者自发组织的、完全免费且开放的学习社区。我们相信通过集体协作与开放共享,可以打破技术垄断,创造一个更公平的学习环境。
{t("tagline")}
</p>
<div className="flex space-x-2">
<a
href="https://github.com/involutionhell"
target="_blank"
rel="noopener noreferrer"
aria-label="访问 GitHub"
title="访问 GitHub"
aria-label={t("github.ariaLabel")}
title={t("github.ariaLabel")}
data-umami-event="social_click"
data-umami-event-platform="github"
data-umami-event-location="footer"
className="w-12 h-12 flex items-center justify-center border border-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)] transition-all text-[var(--foreground)]"
>
<Github className="h-5 w-5" />
<span className="sr-only">访问 GitHub</span>
<span className="sr-only">{t("github.srOnly")}</span>
</a>
<a
href="https://discord.com/invite/6CGP73ZWbD"
target="_blank"
rel="noopener noreferrer"
aria-label="加入 Discord 社区"
title="加入 Discord 社区"
aria-label={t("discord.ariaLabel")}
title={t("discord.ariaLabel")}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Footer 里取的翻译 key 与 messages/* 不匹配:当前使用了 taglinegithub.ariaLabel/github.srOnlydiscord.ariaLabel/discord.srOnly,但 messages/zh.json/en.jsonfooter 下是 branda11y.github/a11y.discord。这会触发 next-intl missing message key(并在生产环境显示 key 文本)。建议统一 key:要么把代码改成读 brand + a11y.*,要么把消息文件改成与代码一致的结构。

Copilot uses AI. Check for mistakes.
Comment thread app/components/Hero.tsx
Comment on lines 58 to 99
@@ -79,21 +82,19 @@ export function Hero() {
</div>

<div className="border border-[var(--foreground)] p-6 bg-[var(--foreground)] text-[var(--background)]">
<h3 className="font-serif text-2xl mb-4">Join the Resistance</h3>
<h3 className="font-serif text-2xl mb-4">{t("join.title")}</h3>
<p className="font-body text-sm mb-6 opacity-80">
Connect with thousands of developers who are reclaiming their
passion for technology.
{t("join.body")}
</p>
<Link
href="/docs/ai"
className="block w-full"
// Umami 埋点: Hero CTA 按钮点击
data-umami-event="navigation_click"
data-umami-event-region="hero_cta"
data-umami-event-label="Access Articles"
>
<button className="w-full py-3 border border-[var(--background)] font-sans text-xs uppercase tracking-widest hover:bg-[var(--background)] hover:text-[var(--foreground)] transition-all cursor-pointer">
Access Articles / 访问文章
{t("cta")}
</button>
</Link>
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hero 里使用了 t("tagline")t("cta"),但 messages/* 中对应 key 是 hero.missionhero.cta.accesscta 是对象不是字符串)。这会导致 missing key / 渲染出错。建议改为读取现有 key(例如 missioncta.access),或同步调整消息文件结构。

Copilot uses AI. Check for mistakes.
Comment on lines 187 to 198
{/* 跳转的投稿指南 */}
<a
href="https://github.com/InvolutionHell/involutionhell?tab=contributing-ov-file#%E6%8A%95%E7%A8%BF%E6%8C%87%E5%8D%97"
target="_blank"
rel="noopener noreferrer"
aria-label="查看投稿指南"
title="查看投稿指南"
aria-label={t("guideAriaLabel")}
title={t("guideAriaLabel")}
className="absolute top-0 right-0 flex h-10 w-10 translate-x-1/2 -translate-y-1/2 items-center justify-center border border-[var(--foreground)] bg-[var(--background)] text-[var(--foreground)] font-mono hover:bg-[#CC0000] hover:text-white transition-colors z-20"
>
<span className="text-sm font-bold">?</span>
<span className="sr-only">查看投稿指南</span>
<span className="sr-only">{t("guideAriaLabel")}</span>
</a>
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contribute 使用了 t("guideAriaLabel") 作为投稿指南链接的 aria-label/title,但 messages/*contribute 命名空间下没有这个 key(当前在 hero.cta.guideAriaLabel 才存在)。这会触发 missing key。建议:把该文案移动/复制到 contribute 命名空间(例如 contribute.guideAriaLabel),或把这里的 namespace/key 改为实际存在的位置。

Copilot uses AI. Check for mistakes.
Footer: tagline→brand, github/discord.ariaLabel/srOnly→a11y.github/a11y.discord
Hero: tagline→mission, cta→cta.access
Contribute: 字典补 guideAriaLabel key(zh+en),不依赖 hero namespace
layout: html lang 硬编码改为 locale 驱动,注释更新为 next-intl 描述
@longsizhuo longsizhuo merged commit 4c650d8 into main Apr 16, 2026
6 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants