Next.js DApp 国际化路由:多语言链上术语库与动态内容切换

📅 2026/7/8 14:37:16
Next.js DApp 国际化路由:多语言链上术语库与动态内容切换
Next.js DApp 国际化路由多语言链上术语库与动态内容切换一、链上术语的一致性问题当滑点不是 Slippage 时DApp 的多语言支持比传统 Web 应用多了一层复杂性链上术语在不同语言中的标准化程度差异巨大。在中文社区DeFi 用户说滑点、无常损失、收割英文社区用 Slippage、Impermanent Loss、Yield Farming。这些术语不是简单的字面翻译问题 —— 它们背后映射到同一套链上逻辑但不同语言用户对术语的理解边界和应用习惯完全不同。更大的挑战来自内容层面。DApp 的 UI 里充斥着链上实时数据TVL 数字、APY 比率、代币名称。这些内容在切换语言时不仅要翻译标签文本还要保持数字格式、日期格式、甚至货币符号的本地化一致。比如中文用户更习惯看万/亿为单位的大数英文用户习惯 K/M/B 缩写。Next.js 的 App Router 在 v13.4 提供了内置的 i18n 路由支持结合next-intl可以在路由层面实现语言切换同时保持服务端渲染能力。这对 DApp 特别重要 —— 链上数据在服务端渲染时可以一次性完成多语言格式化不需要客户端二次 hydration 导致的闪烁。flowchart TD A[用户请求 /zh-CN/swap] -- B{Next.js 中间件} B -- C[解析 locale: zh-CN] C -- D[加载 zh-CN 术语库] D -- E[SSR 渲染页面] E -- F[注入链上数据 本地化格式] F -- G[返回 HTML] H[用户切换语言] -- I[路由跳转 /en/swap] I -- B subgraph 术语库缓存 J[(链上术语 JSON)] K[(UI 标签 JSON)] end D -.- J D -.- K二、路由架构与术语库设计原理Next.js App Router 的 i18n 实现依赖两个核心机制中间件路由重写和服务端 locale 上下文传播。当用户访问/zh-CN/swap时中间件解析 locale 参数将其注入到x-next-intl-localecookie 中然后通过 React Context 在服务端组件树中传递。对于链上术语库不建议使用运行时翻译 API会影响首屏渲染速度而是在构建时生成多语言 JSON 文件。每套语言文件包含两个维度UI 标签翻译和链上术语映射。UI 标签是常规的键值翻译链上术语则采用三层结构术语英文原名 → 中文约定译名 → 使用场景说明。sequenceDiagram participant User as 用户浏览器 participant MW as Next.js 中间件 participant SSR as 服务端组件 participant RPC as 链上 RPC participant Dict as 术语库 User-MW: GET /zh-CN/pool/0x1234 MW-MW: 解析 locale zh-CN MW-SSR: 传递 locale context SSR-Dict: 加载 zh-CN 术语库 SSR-RPC: 查询池子数据 RPC--SSR: 返回原始数据 SSR-SSR: 格式化数字、翻译术语 SSR--USer: 渲染 HTML User-MW: 语言切换 → /en/pool/0x1234 MW-SSR: locale en Note over SSR: 重新加载 en 术语库三、完整工程实践3.1 项目结构与依赖配置# 项目结构 ├── messages/ │ ├── zh-CN.json # 中文术语库 │ ├── en.json # 英文术语库 │ └── common.json # 共享配置 ├── src/ │ ├── middleware.ts │ ├── i18n/ │ │ ├── request.ts # locale 解析 │ │ ├── routing.ts # 路由配置 │ │ └── navigation.ts │ ├── lib/ │ │ └── terminology.ts # 链上术语工具函数 │ └── app/ │ └── [locale]/ │ ├── layout.tsx │ └── page.tsxnpm install next-intl3.2 术语库设计// messages/zh-CN.json — 链上术语定义 { HomePage: { title: 去中心化交易协议 }, Swap: { title: 兑换, from: 支付, to: 获得, slippage: 滑点容忍度, priceImpact: 价格影响 }, Terminology: { slippage: { display: 滑点, description: 交易执行价格与报价价格之间的偏差比例, usageContext: 交易确认页面的参数设置区域 }, impermanentLoss: { display: 无常损失, description: 做市商在池子价格变化时相对于直接持有的价值损失, usageContext: 流动性池的收益分析页面 }, totalValueLocked: { display: 总锁仓量, shortDisplay: 锁仓, description: 协议中锁定的所有资产的总美元价值, formatRule: locale_format_large_number }, annualPercentageYield: { display: 年化收益率, shortDisplay: 年化, description: 按年化计算的收益率含复利效应, formatRule: locale_format_percentage } } }3.3 中间件配置// src/middleware.ts import createMiddleware from next-intl/middleware; import { routing } from ./i18n/routing; /** * 国际化中间件 * 设计考量 * - 使用 next-intl 内置中间件而非手动实现避免 Cookie/Session 同步问题 * - 默认 locale 使用 zh-CN中文为主要用户群 * - localePrefix 使用 always确保 SEO 友好每个语言版本有独立 URL */ export default createMiddleware(routing); export const config { // 匹配所有非静态资源和非 API 路由 matcher: [/, /(zh-CN|en)/:path*, /((?!api|_next|_vercel|.*\\..*).*)], };3.4 路由配置// src/i18n/routing.ts import { defineRouting } from next-intl/routing; import { createNavigation } from next-intl/navigation; /** * 路由配置 * 设计考量 * - 仅保留 zh-CN 和 en 两个语言减少术语库维护成本聚焦核心用户群 * - defaultLocale 设为 zh-CN未指定语言时自动重定向 * - localePrefix 使用 always避免无前缀路由的 SEO 重复问题 */ export const routing defineRouting({ locales: [zh-CN, en], defaultLocale: zh-CN, localePrefix: always, }); export const { Link, redirect, usePathname, useRouter, getPathname } createNavigation(routing);3.5 请求级别的 locale 解析// src/i18n/request.ts import { getRequestConfig } from next-intl/server; import { routing } from ./routing; /** * 按请求加载对应语言的术语库 * 设计考量 * - 使用 getRequestConfig 而非静态导入每个请求独立加载对应语言文件 * - 提供 locale 验证兜底如果 URL 中的 locale 不在支持列表降级到默认 language */ export default getRequestConfig(async ({ requestLocale }) { let locale await requestLocale; // 兜底不在支持语言列表中的 locale降级为默认语言 if (!locale || !routing.locales.includes(locale as any)) { locale routing.defaultLocale; } return { locale, messages: (await import(../../messages/${locale}.json)).default, }; });3.6 布局组件与语言切换// src/app/[locale]/layout.tsx import { NextIntlClientProvider } from next-intl; import { getMessages } from next-intl/server; import { notFound } from next/navigation; import { routing } from /i18n/routing; import { LocaleSwitcher } from /components/LocaleSwitcher; /** * 根布局组件 * 设计考量 * - 使用 NextIntlClientProvider 包裹整个应用确保客户端组件也能访问翻译 * - getMessages 在服务端执行消息数据直接注入到服务端渲染的 HTML 中 */ export default async function LocaleLayout({ children, params, }: { children: React.ReactNode; params: Promise{ locale: string }; }) { const { locale } await params; if (!routing.locales.includes(locale as any)) { notFound(); } const messages await getMessages(); return ( html lang{locale} body NextIntlClientProvider messages{messages} LocaleSwitcher currentLocale{locale} / {children} /NextIntlClientProvider /body /html ); }3.7 链上术语工具函数// src/lib/terminology.ts import { useFormatter } from next-intl; /** * 链上数据本地化工具 * 设计考量 * - 数字格式化与语言绑定中文用万/亿英文用K/M/B * - 百分比统一使用千分位分隔符的 locale 适配 * - 函数纯客户端调用服务端组件使用 getFormatter() 而非 hook */ const LOCALE_NUMBER_CONFIG: Recordstring, { divisors: { value: number; suffix: string }[]; precision: number; } { zh-CN: { divisors: [ { value: 1e8, suffix: 亿 }, { value: 1e4, suffix: 万 }, ], precision: 2, }, en: { divisors: [ { value: 1e9, suffix: B }, { value: 1e6, suffix: M }, { value: 1e3, suffix: K }, ], precision: 2, }, }; /** * 格式化链上大数 * 设计原因不同语言用户对大数的认知模式不同中文用户习惯万/亿 * 英文用户习惯K/M/B。直接显示原始数字会影响可读性。 */ export function formatLargeNumber(value: number | bigint, locale: string): string { const num typeof value bigint ? Number(value) : value; const config LOCALE_NUMBER_CONFIG[locale] ?? LOCALE_NUMBER_CONFIG[zh-CN]; for (const { value: divisor, suffix } of config.divisors) { if (Math.abs(num) divisor) { return ${(num / divisor).toFixed(config.precision)}${suffix}; } } return num.toLocaleString(locale); } /** * 格式化链上百分比 * 例如0.1234 → 12.34%中文/ 12.34% (英文) */ export function formatPercentage(value: number, locale: string): string { const pct (value * 100).toFixed(2); return ${pct}%; }四、边界分析术语库维护是一个持续负担。每新增一个功能页面就需要在两套语言文件中添加对应的翻译条目。如果一个 20 人的团队只有 1 个人负责 i18n术语库很快会变成信息孤岛。解决思路是在 Issue 模板中强制要求 feature 提案附带中英文术语对照。SEO 的多语言挑战。localePrefix: always意味着每个语言版本独立索引Google 会将它们识别为不同页面。需要确保所有hreflang标签正确配置否则可能被判定为重复内容。动态内容不适合提前翻译。用户在链上创建的代币名称、DAO 提案标题等 UGC 内容无法预先录入术语库。这类内容只能做技术性标记如添加lang属性不能强求翻译。数字格式的精度取舍。中文亿/万的除法格式在数值较小时精度损失可以接受显示 0.87 万 vs 8,700但对于精确到分的金融数据必须回退到全精度显示。不适用场景单语言中文社区 DAppi18n 框架的复杂度不值得引入需要 10 语言的全球化产品next-intl 的消息加载机制在语言过多时会产生显著的包体积膨胀大量实时渲染的链上数据仪表盘服务端渲染的多语言处理会显著增加 TTFB五、总结维度要点路由策略Next.js App Router next-intl 中间件localePrefix: always保证 SEO术语库设计三层结构原名→译名→使用场景 构建时静态 JSON避免运行时翻译 API数字格式locale 感知的格式化函数中文万/亿缩写 vs 英文K/M/B缩写渲染策略SSR 优先术语库在服务端直接注入 HTML避免客户端 hydration 闪烁维护成本两套语言文件的持续维护是最大负担需要流程约束和 Review 机制禁用场景单语言社区 DApp、10 语言全球化产品、高实时性数据仪表盘