ReaderX 的官方网站源码(SolidStart + Nitro,部署到 Cloudflare Workers)。
站点支持 中文(zh-CN) 与 英文(en) 两种语言,首次访问按访客设备自动判断, 也可以随时手动切换。
优先级从高到低(服务端与浏览器共用同一套规则,实现在 src/i18n/locale.ts):
- Cookie
readerx-lang—— 用户在页面上手动选过,最优先,刷新与跳转都不该被改回去; - 设备语言 —— 服务端读请求头
Accept-Language(按 q 权重排序),浏览器读navigator.languages; - 兜底 —— 设备语言里既没有中文也没有英文时给英文;连语言都没声明(爬虫、直接 curl)时给中文。
服务端渲染时把结论写进 <html lang>,客户端水合第一帧再从那里读回来,
所以**不会出现「先渲染中文、水合后闪成英文」**的错位。响应带
Vary: Accept-Language, Cookie,避免中间缓存把两种语言混着发。
- 导航栏的地球按钮(下拉菜单,
src/components/LanguageSwitcher.tsx的LanguageMenu)—— 手机上(<sm)这颗按钮让位给分区菜单,语言切换在NavMenu那张浮层里; - 页脚右下角的一排语言胶囊(同文件的
LanguageInline)—— 滚到底、导航栏收起后仍然可用。
切换后立即生效(正文、<html lang>、标题、描述与分享卡片 meta 一起换),
并把选择写进 Cookie,下一次请求服务端就直接按新语言渲染。
顶部那颗玻璃胶囊按宽度分成两段,断点共用 lg(1024px),同一件事只有一个形态可见:
| 宽度 | 分区导航 | 语言 |
|---|---|---|
≥lg |
胶囊里平铺的五个链接(Navbar.tsx 的 <ul>,lg:flex) |
地球按钮 |
<lg |
三横线按钮 + 浮层(NavMenu.tsx,lg:hidden) |
sm–lg 用地球按钮;<sm(手机)在浮层底部 |
另外两条与手机有关的口径:
- 触屏端不参与收起:胶囊的灵动岛收起只在有 hover 的设备上发生
(
Navbar.tsx里collapsed带hasHover()守卫)。触屏没有 hover, 收起后标志全称、分区入口、语言按钮都会淡出且拉不回来,等于导航内容显示不全; - 浮层都挂在
document.body下:胶囊要overflow-hidden才能做收起动画, 挂里面会被裁掉。定位与「点外面 / Esc 关闭」的实现在src/lib/anchoredMenu.ts, 语言菜单与分区菜单共用同一份。
| 位置 | 内容 |
|---|---|
src/i18n/messages.ts |
全部界面文案,中文一份、英文一份 |
src/i18n/locale.ts |
支持的语言、语言名、判定与 Cookie 的读写 |
src/i18n/keys.ts |
「一段文案 + 一个图标 / 示意图」这类条目的 id |
src/i18n/index.tsx |
I18nProvider / useI18n():dict、t、locale、setLocale |
src/i18n/document.ts |
切语言后同步 <html lang> 与文档级 meta |
几条约定:
- 普通文案用
dict()直接取(dict().hero.titleLine1),键路径有类型检查; - 带变量的整句统一放在字典末尾的
t里,用t("versionWithDate", { version, date })取 —— 各语言语序不同,拆成几段 JSX 拼起来译文会很别扭; - 和图标 / 界面示意图配对的条目(功能卡片、界面巡览、书源能力、平台与产物) 按 id 组织:顺序与图标写在组件里,文案按同一个 id 从字典取, 不会出现「书架的文字配着设置页的截图」这种编译期发现不了的问题;
- 英文那份标注成
Messages类型(由中文那份推导),漏翻一条、多翻一条、产物 id 写错都会在tsc阶段报错。
src/i18n/locale.ts:加进LOCALES,补LOCALE_LABELS(用该语言自己的写法);src/i18n/messages.ts:照着中文那份加一份完整译文;- 平台相关的英文技术标签(
arm64-v8a/AppImage之类)本就各语言一致,不用动。
pnpm install
pnpm dev # 本地开发
pnpm build # 构建(Node 产物)
pnpm build:cloudflare && npx wrangler deploy # 部署到 Cloudflare Workers应用仓库:https://github.com/zilorn/readerx
用户进入网页时,服务端使用 GitHub Releases API 检查最新稳定版本(无稳定版时使用预发布版),版本信息缓存 10 分钟以减少 GitHub 限流。获取版本后通过 Nitro 的 Cloudflare waitUntil 在后台预缓存该版本的安装包,三个文件一组,下载链接统一指向本站 /api/download?tag=…&name=…。
下载接口只允许当前 Release API 返回的文件,不能代理任意 URL。命中时从 Cloudflare Cache API 返回文件;未命中时先完整写入缓存,再返回缓存文件。同一 Worker 实例中的并发填充会合并,安装包以流写入,不加载到内存。支持缓存文件的 Range 请求。缓存写入失败时由本站流式代理 GitHub 文件;GitHub 不可用时返回 502,用户可以重试或使用页面上的 GitHub Releases 入口。
无需新增 R2、KV 或其他绑定,现有构建和部署命令即可使用。需要注意:
- Cache API 是各 Cloudflare 数据中心独立的临时缓存,可能提前淘汰;缓存有效期为 24 小时,每次页面请求都会检查并补充缺失文件。
waitUntil在响应结束后最多运行 30 秒,较大的文件可能无法在后台完成预缓存,后续下载会再次填充。并非永久保存、全球同步的文件镜像。- 单文件按普通 Cloudflare 套餐的 512 MiB 缓存上限处理,超过上限通过本站代理。
- 版本更新后旧下载链接返回 404,需要刷新页面获取新版链接。
- 本地 Node 开发环境没有 Cloudflare Cache API,会通过本站代理下载。实际缓存效果应在正式部署域名验证;本地模拟不代表线上缓存命中。
验证:pnpm test、pnpm build:cloudflare。下载响应携带 Content-Disposition 和 Content-Length,不会缓存 GitHub 的错误响应。