- 前端
- Web框架
- SSR
- 前端构建
- 插件系统
- 微前端
- 跨平台
【免费下载链接】ice
🚀 ice.js: The Progressive App Framework Based On React(基于 React 的渐进式应用框架)
导读
本文围绕 ice.js 官方提供的@ice/plugin-i18n国际化插件展开,讲解如何在基于 React 的渐进式应用框架 ice.js 3 中快速开启多语言能力。读完本文,你将掌握国际化路由的自动生成原理、useLocale()/withLocale()等运行时 API 的用法、偏好语言识别与 Cookie 持久化机制、SSR/SSG 下的多语言 HTML 生成,以及禁用 Cookie 等隐私场景的处理方案。
插件核心特性一览
ice.js 官方提供的 i18n 国际化插件,可以在不引入任何第三方 i18n 库的情况下,为应用快速开启国际化能力,其核心特性包括:
- 自动处理和生成国际化路由:为每个非默认语言自动生成带语言前缀的路由;
- 完美支持 SSR 和 SSG:在服务端渲染与构建时静态生成阶段都能输出对应语言的页面,以获得更好的 SEO 优化;
- 自动重定向到偏好语言页面:根据用户偏好(Cookie、浏览器语言、Accept-Language)自动跳转到对应语言的国际化路由;
- 不耦合任何 i18n 库:插件只负责路由、语言状态与持久化,具体文案翻译交给 react-intl、react-i18next 等任意你熟悉的库来实现。
插件在仓库中的实现位于 packages/plugin-i18n,完整的可用示例工程是 examples/with-i18n。
提示:如果应用不需要国际化路由,仅想在应用内支持多语言切换,可以参考 examples/with-antd5 与 examples/with-fusion 两个示例。
快速开始
首先,在终端执行以下命令安装插件:
$ npm i @ice/plugin-i18n -D然后在ice.config.mts中添加插件和选项:
import { defineConfig } from '@ice/app'; import i18n from '@ice/plugin-i18n'; export default defineConfig({ plugins: [ i18n({ locales: ['zh-CN', 'en-US', 'de'], defaultLocale: 'zh-CN', }), ], });上面的en-US、zh-CN是国际化语言的缩写,它们均遵循标准的 UTS 语言标识符。比如:
zh-CN:中文(中国)zh-HK:中文(香港)en-US:英文(美国)de:德文
插件选项的校验逻辑
从源码 packages/plugin-i18n/src/index.ts 可以看到,插件在setup阶段会先调用checkPluginOptions对配置做基本校验:
locales必须是数组,否则报错The plugin option locales type should be array...;defaultLocale必须是字符串,否则报错The plugin option defaultLocale type should be string...。
两者任一不合法,构建过程都会以process.exit(1)直接终止,尽早暴露配置问题。
构建时自动生成的内容
插件通过generator.addRenderFile将 templates/plugin-i18n.ts.ejs 渲染为应用内的plugin-i18n.ts,实际生成的内容大致等价于:
export function getDefaultLocale() { return 'zh-CN'; } export function getAllLocales() { return ['zh-CN', 'en-US', 'de']; }随后通过generator.addExport把getDefaultLocale、getAllLocales暴露为ice包导出,把withLocale、useLocale暴露为@ice/plugin-i18n/runtime导出,同时把i18nConfig注入到customRuntimeOptions中供运行时读取。
国际化路由
国际化路由是指在页面路由地址中包含当前页面的语言,一个国际化路由对应一种语言。
假设现在插件的选项配置是:
import { defineConfig } from '@ice/app'; import i18n from '@ice/plugin-i18n'; export default defineConfig({ plugins: [ i18n({ locales: ['zh-CN', 'en-US', 'nl-NL'], defaultLocale: 'zh-CN', }), ], });假设有一个页面src/pages/home.tsx,那么将会一一对应自动生成以下路由:
/home:显示zh-CN语言,默认语言对应的路由不包含语言前缀;/en-US/home:显示en-US语言;/nl-NL/home:显示nl-NL语言。
访问不同的路由,将会显示该语言对应的页面内容。
路由生成原理
从源码 packages/plugin-i18n/src/index.ts 可以看出,插件通过addRoutesDefinition钩子注册路由定义。它先将locales中不等于defaultLocale的语言收集为prefixedLocales,再遍历框架解析出的nestedRouteManifest(即src/pages目录对应的路由清单),为每个带前缀的语言生成形如/${locale}/${route.path}的新路由,并把嵌套子路由一并递归注册:
const prefixedLocales = locales.filter(locale => locale !== defaultLocale); // ... prefixedLocales.forEach(prefixedLocale => { options.nestedRouteManifest.forEach(route => { const newRoutePath = `${prefixedLocale}${route.path ? `/${route.path}` : ''}`; defineRoute(newRoutePath, route.file, { index: route.index }, () => { route.children && defineChildrenRoutes(route.children, prefixedLocale); }); }); });也就是说,默认语言的页面复用原始路由(不带前缀),而每个非默认语言会自动复制出一整套带语言前缀的路由树,开发者无需手工维护。
获取语言信息
getAllLocales()
用于获取当前应用支持的所有语言:
import { getAllLocales } from 'ice'; console.log(getAllLocales()); // ['zh-CN', 'en-US']getDefaultLocale()
用于获取应用配置的默认语言:
import { getDefaultLocale } from 'ice'; console.log(getDefaultLocale()); // 'zh-CN'useLocale()
在 Function 组件中使用useLocale()Hook API,它的返回值是一个数组,包含两个值:
- 当前页面的语言;
- 一个 set 函数用于更新当前页面的语言。注意,默认情况下调用此 set 函数时,同时会更新 Cookie 中
ice_locale的值为当前页面的语言。这样,再次访问该页面时,服务端请求能得知当前用户之前设置的偏好语言,以便返回对应语言的页面内容。
import { useLocale } from 'ice'; export default function Home() { const [locale, setLocale] = useLocale(); console.log('locale: ', locale); // 'en-US' return ( <> {/* 切换语言为 zh-CN */} <div onClick={() => setLocale('zh-CN')}>Set zh-CN</div> </> ) }从实现上看,useLocale()的本质是读取 I18nContext 这个 React Context 的值。I18nProvider初始化时会根据当前pathname解析出 URL 中携带的语言(通过normalizeLocalePath),解析不到时回退到defaultLocale;setLocale内部在非禁用 Cookie 的情况下会先调用setLocaleToCookie写入 Cookie,再更新 React 状态,从而触发页面重渲染。
withLocale()
使用withLocale()方法包裹 Class 组件,组件的 Props 会包含locale和setLocale()函数,可以查看和修改当前页面的语言。注意,默认情况下调用setLocale()会更新 Cookie 中ice_locale的值为当前页面的语言。
import { withLocale } from 'ice'; function Home({ locale, setLocale }) { console.log('locale: ', locale); // 'en-US' return ( <> {/* 切换语言为 zh-CN */} <div onClick={() => setLocale('zh-CN')}>Set zh-CN</div> </> ) } export default withLocale(Home);withLocale的实现同样基于 Context:源码 中它是一个高阶组件,内部调用useLocale()取出[locale, setLocale]后作为 Props 透传给被包裹的组件,因此它实际上也可以用于 Function 组件。
切换语言
推荐使用setLocale()方法配合<Link>组件或者useNavigate()方法进行语言切换。
方式一:使用<Link />
import { useLocale, getAllLocales, Link, useLocation } from 'ice'; export default function Layout() { const location = useLocation(); const [activeLocale, setLocale] = useLocale(); return ( <main> <p><b>Current locale: </b>{activeLocale}</p> <b>Choose language: </b> <ul> { getAllLocales().map((locale: string) => { return ( <li key={locale}> <Link to={location.pathname} onClick={() => setLocale(locale)} > {locale} </Link> </li> ); }) } </ul> </main> ); }方式二:使用useNavigate()
import { useLocale, useNavigate, useLocation } from 'ice'; export default function Layout() { const [, setLocale] = useLocale(); const location = useLocation(); const navigate = useNavigate(); const switchToZHCN = () => { setLocale('zh-CN'); navigate(location.pathname); } return ( <main> <div onClick={switchToZHCN}> 点我切换到中文 </div> </main> ); }路由跳转时自动拼接语言前缀
为什么使用<Link>/useNavigate()而非直接修改地址?因为插件在运行时对history的push/replace做了劫持(见 packages/plugin-i18n/src/runtime/hijackHistory.tsx)。跳转时会自动检测当前偏好语言,并为非默认语言拼上语言前缀,保证语言切换后访问的路由仍然是正确的国际化路由。
examples/with-i18n示例工程就采用了方式一:在 layout.tsx 中遍历getAllLocales()渲染语言切换列表,并利用react-intl的<IntlProvider>配合 locales.ts 中的文案映射(zh-CN对应"普通按钮"、en-US对应"Normal Button")实现真正的翻译展示,验证了"插件不耦合任何 i18n 库"的设计。
路由自动重定向
路由自动重定向是指:如果当前访问的页面是根路由/,将会根据当前语言环境自动跳转到对应的国际化路由。
默认情况下,路由自动重定向的功能是关闭的。如果需要开启,则需要加入以下内容:
import { defineConfig } from '@ice/app'; import i18n from '@ice/plugin-i18n'; export default defineConfig({ plugins: [ i18n({ locales: ['zh-CN', 'en-US', 'de'], defaultLocale: 'zh-CN', + autoRedirect: true, }), ], });其中,语言环境的识别顺序如下:
- CSR:cookie 中
ice_locale的值 >window.navigator.language>defaultLocale - SSR:cookie 中
ice_locale的值 >Request Header中的Accept-Language>defaultLocale
源码中的重定向实现
结合 packages/plugin-i18n/src/runtime/index.tsx,当autoRedirect为true时,插件注册了一个addResponseHandler响应处理器:对每个请求解析 URL,调用detectLocale识别偏好语言,再调用getLocaleRedirectPath判断是否需要重定向。只有当访问的是根路径/(去除 basename 后)且检测到的语言不是defaultLocale时,才返回302 Found并携带location响应头指向/检测到的语言。这也解释了为什么自动重定向只对根路由生效。
语言识别逻辑封装在 detectLocale.ts 中,优先级依次是:
- URL 路径中已携带的语言前缀(
normalizeLocalePath); - Cookie 中的
ice_locale值(getLocaleFromCookie); - 偏好语言(
getPreferredLocale,客户端取window.navigator.languages,服务端解析Accept-Language请求头,使用的是accept-language-parser库); - 回退到
defaultLocale。
部署阶段需要 Node 中间件配合
在部署阶段,路由自动重定向的功能需要配合 Node 中间件使用才能生效。比如:
import express from 'express'; import { renderToHTML } from './build/server/index.mjs'; const app = express(); app.use(express.static('build', {})); app.use(async (req, res) => { const { statusCode, statusText, headers, value: body } = await renderToHTML({ req, res }); res.statusCode = statusCode; res.statusMessage = statusText; Object.entries((headers || {}) as Record<string, string>).forEach(([name, value]) => { res.setHeader(name, value); }); if (body && req.method !== 'HEAD') { res.end(body); } else { res.end(); } });完整的可运行服务端示例见 examples/with-i18n/server.mts,它监听4000端口并设置了basename为/app。对应地,examples/with-i18n/ice.config.mts 中开启了autoRedirect: true和ssr: true,app.tsx 中通过defineAppConfig配置了router.basename: '/app'。
禁用 Cookie
在上面的章节中提到,用户设置的偏好语言存放在 Cookie 中的ice_locale,调用setLocale()时会更新到 Cookie 中,并且路由重定向和路由跳转的时候都依赖ice_locale的值。ice_locale这个 Cookie 名称定义在 packages/plugin-i18n/src/constants.ts 中。
假设有这样一个场景:用户拒绝接受 Cookie,为了保护隐私,就不能把偏好语言写到 Cookie 中了。此时需要做以下配置来禁用 Cookie:
import { defineI18nConfig } from '@ice/plugin-i18n/types'; export const i18nConfig = defineI18nConfig(() => ({ // 可以是一个 function disabledCookie: () => { if (import.meta.renderer === 'client') { return window.localStorage.getItem('acceptCookie') === 'yes'; } return false; }, // 也可以是 boolean 值 // disabledCookie: true, }));这样,就禁用了 Cookie 的写入。在切换语言的时候,需要在state对象中显式传入即将要切换的新语言的值:
import { Link, useLocale } from 'ice'; export default function Home() { const [, setLocale] = useLocale(); return ( <> <Link to="/" onClick={() => setLocale('zh-CN')} state={{ locale: 'zh-CN' }} > 切换到 zh-CN </Link> </> ) }禁用 Cookie 后的运行时行为
defineI18nConfig的类型定义与说明见 packages/plugin-i18n/src/types.ts,它接受I18nAppConfig对象或其工厂函数。运行时插件会读取应用导出的i18nConfig,合并默认值{ disableCookie: false }后计算得到最终的disableCookie值。
当disableCookie为真时,会有两处关键行为变化(源码均在 I18nContext.tsx 与 hijackHistory.tsx):
setLocale()不再写 Cookie,只更新 React 状态;- 路由跳转时不再从 Cookie 探测语言,而是优先读取跳转
state.locale中显式传入的新语言作为前缀拼接依据——这正是上面示例中state={{ locale: 'zh-CN' }}存在的原因。
SSG 多语言静态生成
在开启 SSG 功能后(ice.js 默认开启 SSG,详见 website/docs/guide/basic/ssg.md),插件将根据配置的locales的值,在build阶段生成不同语言对应的 HTML。
比如有以下目录结构,包含about和index两个页面:
├── src/pages │ ├── about.tsx │ └── index.tsx假如插件的配置是:
import { defineConfig } from '@ice/app'; import i18n from '@ice/plugin-i18n'; export default defineConfig({ plugins: [ i18n({ locales: ['zh-CN', 'en-US'], defaultLocale: 'zh-CN', }), ], });那么将会生成 4 个 HTML 文件:
├── build │ ├── about │ │ └── index.html │ ├── en-US │ │ ├── about │ │ │ └── index.html │ │ └── index.html │ ├── index.html也就是说,默认语言只生成一份不带前缀的 HTML,每个非默认语言都会多生成一整套带语言目录的 HTML。结合 SSR/SSG 的能力,搜索引擎可以分别抓取各语言版本的页面,从而实现更好的 SEO 效果。
集成测试的验证
仓库中的集成测试 tests/integration/with-i18n.test.ts 对上述行为做了自动化验证:
- 构建
with-i18n示例后,断言build目录下的 HTML 文件列表精确等于['blog.html', 'blog/a.html', 'en-US.html', 'en-US/blog.html', 'en-US/blog/a.html', 'index.html'],印证了"每个非默认语言生成整套 HTML"的规则; - 访问
/页面断言按钮文案为"普通按钮"(zh-CN); - 访问
/en-US.html页面断言按钮文案为"Normal Button"(en-US)。
该示例工程同时开启了ssr: true与autoRedirect: true,是研究 SSR、SSG、国际化路由三者协同工作方式的最佳参考。
插件选项一览
locales
- 类型:
string[] - 必填
用于声明该应用支持的语言。插件在构建阶段会为除默认语言外的每种语言复制生成一套带前缀的国际化路由,并在 SSG 构建时生成对应的语言目录 HTML。
defaultLocale
- 类型:
string - 必填
声明该应用默认的语言。需要注意的是,locales数组必须包含defaultLocale的值。默认语言对应的路由不包含语言前缀;当访问根路由/且检测到用户偏好语言时,自动重定向的目标就是defaultLocale之外的其他语言路径。
autoRedirect
- 类型:
boolean - 默认值:
false
默认不会自动重定向到用户偏好语言对应的页面。如果设置为true,在生产环境下一般需要配合 Node 中间件一起使用才能生效(详见上文"路由自动重定向"一节)。开启后,插件通过响应处理器对根路由/返回302重定向响应,跳转到用户偏好语言对应的国际化路由。
与业务代码的组合实践
把以上 API 组合起来,一个完整的国际化页面通常由三部分组成(参考 examples/with-i18n 的工程结构):
- 文案资源:维护一份
locales.ts之类的语言包,如{'en-US': { buttonText: 'Normal Button' }, 'zh-CN': { buttonText: '普通按钮' }}; - 语言状态注入:在布局组件中调用
useLocale()获取当前语言,并借助任意 i18n 库(如react-intl)的 Provider 注入文案;语言切换 UI 遍历getAllLocales()渲染所有可用语言,点击时调用setLocale()切换; - 路由切换:切换语言后通过
<Link>或useNavigate()跳转,插件会自动拼接语言前缀并持久化偏好(默认写入ice_localeCookie)。
这样,路由层面由插件全权托管,翻译与格式化交给任意 i18n 库,两者职责清晰、互不耦合。这也是"渐进式应用框架"理念在国际化能力上的体现:需要时引入插件即可获得开箱即用的多语言能力,同时保留对具体实现方案的自由选择权。
- 前端
- Web框架
- SSR
- 前端构建
- 插件系统
- 微前端
- 跨平台
【免费下载链接】ice
🚀 ice.js: The Progressive App Framework Based On React(基于 React 的渐进式应用框架)
相关推荐
purejs-onepage-scroll高级技巧:自定义动画与事件回调
purejs onepage scroll高级技巧:自定义动画与事件回调 purejs onepage scroll是一款轻量级的JavaScript单页滚动库
前端Web框架SSR前端构建插件系统微前端跨平台Gutenberg 国际化(i18n)实战指南:让 WordPress 区块插件走向多语言
Gutenberg 国际化(i18n)实战指南:让 WordPress 区块插件走向多语言 国际化(Internationalization,缩写为 i18n
后端前端ice.js 国际化(i18n)最佳实践:@ice/plugin-i18n 插件完整指南与源码解析
ice.js 国际化(i18n)最佳实践:@ice/plugin i18n 插件完整指南与源码解析 导读 @ice/plugin i18n 是 ice.js 3
前端Web框架SSR前端构建插件系统微前端跨平台
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考