opencode RTL 支持开发指南:从方向感知布局到 Electron 标题栏的完整实践
OpenCode 的 Web 应用与桌面端需要同时服务 LTR(左到右)与 RTL(右到左)用户,仓库中的 rtl-aware-development Skill(.opencode/skills/rtl-aware-development/SKILL.md)把这一需求沉淀为一套可复用的开发准则。本文以该 Skill 为主体,逐条拆解其核心规范,并结合 packages/app、packages/desktop、packages/ui 中的真实源码说明 opencode 是如何落地“方向与语言解耦、逻辑属性优先、物理坐标显式处理”的完整 RTL 支持方案的。读完后,你可以在任意 opencode 前端模块中正确地实现或评审 RTL/LTR 行为。
核心原则:方向独立于语言
Skill 的开篇即给出总纲:Treat direction as independent from language——把书写方向视为独立于语言的选择,并且测试时必须覆盖“英文 + LTR”“英文 + 强制 RTL”以及真实 RTL 语言与混合书写的组合。
这一点直接决定了实现方式:不能靠“换一个 RTL 语言包”来验证方向行为,因为真实用户的界面语言与阅读方向是可以解耦的。opencode 源码正是这么做的——在 language.tsx 中,方向由两部分推导:
- 语言本身的默认方向:
RTL_LOCALES集合(ar、ur、pa、fa、dv)命中则"rtl",否则"ltr",见 language.tsx#L23-L27; - 用户可显式覆盖:
setLayout("direction", ...)提供方向覆盖,且当覆盖值与语言默认方向一致时会自动归一为undefined(即回到“跟随语言”),见 language.tsx#L237-L239。
因此“选择阿拉伯语”不等于“强制 RTL”,反之亦然。这也正是 Skill 所强调的“Do not change the selected locale merely to force RTL”。
在文档上设置 lang 与 dir,并传播到弹层组件
Skill 第一条准则:在 document 上设置 lang 和 dir,并把方向传播到承载 Portal 化菜单和弹层的组件 Provider 中。
opencode 的落地分两层。第一层是文档级:LanguageProvider 中有一个副作用,持续把语言与方向同步到 <html> 元素并写入 Cookie(用于 SSR/首屏),见 language.tsx#L207-L213:
createEffect(() => {
if (typeof document !== "object") return
const value = locale()
document.documentElement.lang = intl()
document.documentElement.dir = direction()
document.cookie = cookie(value)
})
第二层是组件级。Solid 的 Portal 化组件(下拉菜单、Popover)脱离文档 DOM 位置后仍需要知道方向,opencode 通过一个专门的 layoutLocale 推导值解决:
const layoutLocale = createMemo(() => {
if (!layout.direction) return intl()
// Kobalte derives menu direction from locale rather than accepting a direction override.
return layout.direction === "rtl" ? "ar" : "en"
})
注释直说了原因:Kobalte 的 I18n Provider 从 locale 推导菜单方向,而不是接受方向覆盖,因此用 "ar" / "en" 这两个代理 locale 把方向语义“翻译”给 UI 库。该值经由 app.tsx#L237-L241 注入 I18nProvider:
<I18nProvider
value={{ locale: language.intl, layoutLocale: language.layoutLocale, t: language.t, plural: language.plural }}
>
最终在 packages/ui/src/context/i18n.tsx#L64 中,layoutLocale 优先于 locale 生效,菜单等 Portal 组件的方向由此与主文档保持一致。
保持语义化的 DOM 与焦点顺序
Skill 指出:Flexbox 和 Grid 本身会遵循 dir,不要为了镜像布局而添加 row-reverse、CSS order 或颠倒的 DOM 结构,以保持 DOM 顺序与焦点顺序的语义一致性。
这是一条约束类准则而非实现类:它的价值在于评审检查项——当某段代码出现 flex-row-reverse 或 order: -1 时,应优先怀疑它是在硬编码镜像 LTR 布局,而不是响应 dir。在 opencode 的组件树中(例如 titlebar-tab-strip.tsx 这类横向条带组件),布局均依赖标准 flex 方向,方向切换由 dir 属性自动完成,无需额外反向样式。
优先使用逻辑属性,物理值只留给真正物理的场景
Skill 中给出了最典型的 CSS 对照示例,这是 RTL 改造的高频操作点:
/* Avoid */
padding-left: 12px;
right: 0;
border-right: 1px solid;
text-align: left;
/* Prefer */
padding-inline-start: 12px;
inset-inline-end: 0;
border-inline-end: 1px solid;
text-align: start;
逻辑属性(padding-inline-start、inset-inline-end 等)随 dir 自动翻转,一份样式同时服务两种方向。Skill 同时划定了物理坐标的合法保留场景:指针位置、Canvas 几何、原生窗口控件等确实与屏幕物理位置绑定的部分。opencode 的标题栏实现正是这个边界的范例(见下文 Electron 章节)。
隔离混合方向文本
Skill 要求:未知方向的文本用 dir="auto" 或 <bdi> 隔离;代码、URL、ID、文件系统路径必须保持 LTR,但不要为此把外层组件整体强制为 LTR。原文档给出的示例:
<span class="file-row"><bdi dir="auto">README.md</bdi></span> <bdi dir="ltr"><code>C:\src\app.ts</code></bdi>
这在 opencode 这类编码代理产品里尤为关键:会话面板、文件树、终端面板会同时渲染用户输入的任意语言文本与代码/路径。典型风险是 RTL 段落中嵌入 C:\src\app.ts 这类路径时,Unicode 双向算法会把反斜杠位置排错;用 <bdi dir="ltr">(或 unicode-bidi: isolate)把路径作为整体隔离,即可保证路径字符序稳定,同时不影响外层 RTL 布局。
镜像“方向语义”而不是“所有图像”
Skill 明确区分了该镜像与不该镜像的元素:
- 可以/需要镜像:返回/前进、上一页/下一页、折叠展开指示器、缩进方向、方向性进度条;
- 不能镜像:品牌 Logo、时钟、媒体控制按钮、图表、文本本身;
- 需要显式反向:物理方向的渐变、
translateX、SVG transform、动画增量(delta)。
最后一类最容易被遗漏:translateX(20px) 在 LTR 下表示“向右移动”,但在语义上若表示“进入”,RTL 下应为负值。评审时可以把“代码里所有 translateX、gradient(to right, ...)、SVG transform”作为检查清单,逐条确认它表达的是物理方向还是语义方向。
交互映射:clientX 永远是物理的
Skill 指出的核心矛盾:clientX 等指针坐标是物理的,而“逻辑边”的 resize 需要方向感知的增量换算——在 LTR 下拖动右边缘,delta = clientX - startX 直接就是宽度增量;在 RTL 下同一边缘在视觉上位于左侧,增量需要取反。此外,语义化的“上一个/下一个”键盘控件在 RTL 下可能交换 ArrowLeft/ArrowRight,具体应遵循对应的 WAI-ARIA 组件模式(如 Window splitter 模式)。
这条准则指导 resize 手柄、splitter、横向滚动条等组件的实现:事件坐标不做转换,但“把物理 delta 映射到逻辑尺寸变化”的那一层必须引入方向分支。
不要假设 LTR 的滚动行为
Skill 提示:RTL 页面的 scrollLeft 可以从 0 开始并向负值增长(滚动起点在右侧),跨方向代码不能假设 scrollLeft >= 0 或“滚到尽头时 scrollLeft === scrollWidth - clientWidth”。推荐做法是用 scrollIntoView({ inline: "nearest" }) 或经过双向验证的方向归一化辅助函数。
这一条对应 Skill 测试矩阵中的“both LTR and RTL scroll endpoints”——验证时必须检查两个方向的滚动端点行为,而不是只看 LTR。
Electron 标题栏:物理与逻辑的边界
Skill 对 Electron 标题栏的要求非常具体:优先使用原生 caption 控件,用 titleBarOverlay + env(titlebar-area-*) 定义安全内容矩形;Windows/macOS 的原生控件避让区和 trafficLightPosition 保持物理坐标;矩形内部的应用导航则使用逻辑布局;标题栏内可交互子元素标记 app-region: no-drag。
opencode 桌面端的主进程实现与之完全对应,见 windows.ts#L186-L198:
...(process.platform === "darwin"
? {
titleBarStyle: "hidden" as const,
trafficLightPosition: { x: 14, y: 14 }, // 物理坐标:红绿灯按钮位置
}
: {}),
...(process.platform === "win32"
? {
frame: false,
titleBarStyle: "hidden" as const,
titleBarOverlay: overlay({ mode }), // 原生 caption 控件覆盖层
}
: {}),
- macOS:隐藏标题栏,
trafficLightPosition用{ x: 14, y: 14 }这一物理坐标避让红绿灯按钮; - Windows:无边框窗口叠加原生
titleBarOverlay,把最小化/最大化/关闭交给系统绘制,从而天然获得正确方向与高 DPI 行为。
渲染进程侧则用 CSS 环境变量读取系统给出的安全矩形。titlebar.tsx#L184-L185 中:
width: windows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
env(titlebar-area-width, <fallback>) 优先取 Electron 注入的实际可用宽度,取不到时回退到按控件宽度估算的值——安全矩形内部的应用导航随后按逻辑属性布局,方向翻转时自动适配。而可交互的标题栏元素则显式声明不参与窗口拖拽,如 titlebar-tab-strip.tsx#L291 的 [app-region:no-drag]。
验证:看行为,而不只是截图
Skill 的收尾准则:验证行为,而非仅截图——需要检查计算样式(computed styles)、伪元素几何、命中区域、焦点顺序、键盘行为、子菜单方向、缩放/分级、以及 LTR 与 RTL 双方向的滚动端点。
Skill 给出的测试矩阵是验收清单:
| 组合 | 覆盖点 |
|---|---|
| 英文 + LTR | 基线行为 |
| 英文 + 强制 RTL | 方向与语言解耦 |
| 真实 RTL 语言 + RTL | 真实脚本(含标点/数字) |
| 混合 RTL/LTR 内容、长标签、数字、代码、路径 | 双向文本与 LTR 豁免内容 |
| 键盘、指针 resize、滚动、菜单/子菜单、Electron 标题栏控件,双方向 | 交互与物理/逻辑映射 |
仓库内有一个现成的验证入口:调试栏(debug-bar.tsx#L566-L573)提供一个方向切换开关,点击即在 LTR/RTL 之间翻转(调用上文提到的 language.setDirection),对应测试矩阵中“英文 + 强制 RTL”这一组合,无需切换界面语言即可全量走查上述检查项。
小结
这套 Skill 的核心可归纳为四句话:方向与语言解耦(dir 独立可覆盖,Portal 组件经 layoutLocale 感知方向);布局用逻辑属性、保持语义化 DOM;物理量(指针、控件避让、trafficLightPosition)显式保留,语义量(镜像图标、resize 增量、键盘左右键)显式映射;验证看行为矩阵而非截图。以上每条准则都能在当前仓库中找到对应实现或验证入口,可直接作为 opencode 前端 RTL 改造与 Code Review 的检查标准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00