鸿蒙 PC Markdown 编辑器 ArkWeb 运行时本地化:不重载 CodeMirror 的中英文切换
鸿蒙 PC Markdown 编辑器 ArkWeb 运行时本地化不重载 CodeMirror 的中英文切换鸿蒙 PC Markdown 编辑器通常不会只由原生控件构成。OhMarkdown 的文件、设置和系统能力在 ArkUI源码编辑、预览、命令面板与三方差异运行在离线 ArkWeb。语言切换如果通过重新加载 Web 页面实现会同时重建 CodeMirror 文档、选择区、撤销历史、滚动位置和临时预览代价远高于几段文字更新如果只改变静态 DOM编辑器占位、正在显示的差异和命令搜索又可能停留在旧语言。本文基于公开仓库 https://gitcode.com/VON-/codex_md_oh 的真实代码核心提交为ed13ee0当前设备复核基线为0d8d38b。实现通过强类型消息表、CodeMirrorCompartment、固定 Bridge 入口和可重复重绘在不重载页面的情况下切换中文与英文。Playwright 最终30/30MateBook Pro 2in1 模拟器 ohosTest7/7。文章不把未实现的任意语言包、在线翻译或插件词典描述为现有能力。Web 编辑器中真正需要本地化的对象第一类是可访问名称整个 Markdown 工作区、源码编辑器、预览区、命令面板、命令输入框和候选列表。第二类是稳定可见文案CodeMirror 空文档占位、页面标题、无结果提示。第三类是数据驱动 UI命令标题、分类、关键词三方差异的栏名、图例和计数。第四类是动态反馈图片保存、插入、读取失败与超时。这些对象更新方式不同。普通 DOM 属性可以直接赋值CodeMirror 占位是扩展需要通过状态事务重配命令条目保存在数组中修改后要重新筛选和渲染三方差异的统计由 baseline/local/disk 重新计算不能只换标题图片状态带文件名或路径需要消息函数而不是静态字符串。语言切换不应改变命令 ID、视图模式、文档 sessionId、Bridge 协议或 Markdown 文本。file.save仍然是file.save只是标题从Save Document变成“保存文档”。把可见表达与稳定标识分开是运行时本地化不破坏功能的前提。用接口约束消息表完整性web-editor/src/main.ts定义EditorMessages把所有必须提供的静态文本、动态格式函数和命令翻译纳入类型。中英文对象都必须满足同一接口TypeScript 在构建阶段发现缺失字段而不是等用户打开某个低频面板。interfaceEditorMessages{workspaceLabel:string;sourceEditorLabel:string;previewLabel:string;editorPlaceholder:string;commandPaletteLabel:string;searchCommands:string;availableCommands:string;noMatchingCommands:string;comparisonTitle:string;closeComparison:string;localImageUnavailable:string;imageTimedOut:(path:string)string;savingImage:(name:string)string;conflictCount:(count:number)string;commands:Recordstring,CommandTranslation;}动态文本使用函数避免调用处手工拼接语序。英文${count} conflicts与中文${count} 个冲突由各自词典负责。文件名和路径仍作为参数原样插入不进行危险转义或改写显示时使用textContent或安全属性不进入innerHTML。commands用Recordstring, CommandTranslation允许当前命令表按稳定 ID 查找。它没有强制所有任意字符串键都存在因此setLocale仍做存在判断缺失时保留原始英文作为降级。未来可以把命令 ID 提升为联合类型进一步让编译器验证全覆盖。两套词典与明确回退当前EditorLocale只有en和zh-CN。英文是默认词典简体中文覆盖全部核心 Web 界面。中文命令关键词有意保留部分英文同义词用户在中文界面输入undo或file仍能找到命令这不是漏翻译而是专业编辑器的双语检索策略。constEDITOR_MESSAGES:RecordEditorLocale,EditorMessages{en:{editorPlaceholder:Start writing Markdown...,commandPaletteLabel:Command Palette,noMatchingCommands:No matching commands,commands:{file.save:{title:Save Document,category:File,keywords:write persist}}},zh-CN:{editorPlaceholder:开始编写 Markdown...,commandPaletteLabel:命令面板,noMatchingCommands:没有匹配的命令,commands:{file.save:{title:保存文档,category:文件,keywords:write persist 保存}}}};入口收到任意标签时只判断是否以zh开头其他统一回退en。平台原生层已经先把用户设置限制为简体中文或英文但 Web 再做一次收敛防止旧版本或测试直接调用传入不可用键。回退不是假装支持法语而是保证编辑器仍可用。词典和编辑器脚本一起打入离线单 HTML。语言切换没有网络请求不会在用户文档打开时下载 JSON也不会因为离线而出现空白。两套文本增加的包体可控当前不值得引入动态分包和额外失败点。CodeMirror Compartment 是关键机制CodeMirror 6 的占位文案属于扩展。创建 EditorView 时OhMarkdown 不直接把placeholder(...)固定塞进扩展数组而是用localeCompartment.of(...)包装。这样后续可以发一个 reconfigure effect只替换该扩展保留 EditorState 的文档、selection、history 和其他 compartment。leteditorLocale:EditorLocaleen;constlocaleCompartmentnewCompartment();consteditornewEditorView({parent:editorHost,state:EditorState.create({extensions:[localeCompartment.of(placeholder(getEditorMessages().editorPlaceholder))]})});运行时更新只分发 effecteditor.dispatch({effects:localeCompartment.reconfigure(placeholder(messages.editorPlaceholder))});这和新建 EditorView 有本质区别。重建需要重新注入全文、恢复 selection、重新绑定监听、恢复多个 session 和历史任何遗漏都可能造成数据损失。Compartment 让变更成为 CodeMirror 正常事务状态对象负责保持其余事实。同样的模式已经适合主题、只读模式等动态配置。语言独立使用一个 compartment避免切换占位时覆盖其他扩展。大型文档中也只重配固定扩展不重新解析 Markdown 全文。setLocale 的同步更新顺序setLocale首先收敛词典键然后更新 HTML 语言、标题和各区域 ARIA再改命令数组最后重配 CodeMirror、重绘命令面板和可见差异。顺序确保后续渲染读取的是新词典。functionsetLocale(locale:string):void{editorLocalelocale.toLowerCase().startsWith(zh)?zh-CN:en;constmessagesgetEditorMessages();document.documentElement.langeditorLocale;document.titleeditorLocalezh-CN?OhMarkdown 编辑器:OhMarkdown Editor;workspace.setAttribute(aria-label,messages.workspaceLabel);editorHost.setAttribute(aria-label,messages.sourceEditorLabel);preview.setAttribute(aria-label,messages.previewLabel);commandPalette.setAttribute(aria-label,messages.commandPaletteLabel);commandQuery.placeholdermessages.searchCommands;editorCommands.forEach((command){consttranslatedmessages.commands[command.id];if(translated){command.titletranslated.title;command.categorytranslated.category;command.keywordstranslated.keywords;}});editor.dispatch({effects:localeCompartment.reconfigure(placeholder(messages.editorPlaceholder))});renderCommandPalette();}document.documentElement.lang对辅助技术和文本处理都有意义不能只换页面标题。编辑器、预览与命令面板分别设置 ARIA避免屏幕阅读器仍朗读旧语言上下文。textContent与属性赋值不会把翻译解释为 HTML维持 CSP 和 DOM 安全边界。函数是幂等的。连续调用两次中文不会叠加监听或创建第二个编辑器只再次写入相同属性和扩展。Ability 创建、工作台点击、配置更新和 Web Ready 都可以安全地汇入此入口。命令面板需要翻译标题也翻译检索语料只翻译命令标题还不够。命令面板评分会将标题、分类和关键词归一化后匹配查询如果中文界面仍只有英文关键词用户输入“保存”可能找不到file.save。中文词典因此给每条命令提供中文标题、分类和混合关键词。命令 ID 从不翻译。执行时仍按file.save、workspace.find、view.split路由ArkUI Bridge 不需要知道当前语言。这样自动化、快捷键和安全白名单都基于稳定协议。可见命令排序会随词典改变因为查询语料改变但 enabled 条件和 execute 函数保持原对象。setLocale更新命令数组后立即renderCommandPalette()。若面板当前打开用户不会看到旧列表直到下次输入若查询为中文新关键词立刻参与匹配。空结果提示、输入 placeholder、列表 aria-label 也在同一次调用更新避免半中文状态。Playwright 用中文查询验证实际候选而不只是读取词典对象。这能发现“翻译已写但渲染未重跑”或“标题变了但搜索索引仍缓存旧值”的问题。三方差异必须基于现有数据重绘外部修改比较由 baseline、本地缓冲区和磁盘版本组成。差异面板有标题、关闭按钮、图例、三栏可访问名称、栏头、格式差异说明以及冲突/本地/磁盘计数。语言切换时这些都要同步。setLocale先更新固定 DOM然后判断差异是否可见且activeThreeWayComparison存在。满足条件时再次调用showThreeWayDiff输入仍是内存中的三份文档和 metadata不重新读取磁盘也不改变冲突状态。if(!conflictComparison.hiddenactiveThreeWayComparison){showThreeWayDiff(activeThreeWayComparison.baseline,activeThreeWayComparison.local,activeThreeWayComparison.disk,activeThreeWayComparison.metadata);}重新计算展示比逐个查找所有动态计数文本更可靠。差异算法结果不依赖语言变化的是说明字符串。因为只在面板可见时执行普通语言切换不会承担三方比较成本。大比较已有降级文本也从当前词典读取。重绘不能改变用户选择因为差异面板是只读比较视图真正的 Keep Local、Use Disk、Save As 操作仍在原生层。Web 只展示三份内容不持有文件写权限。图片反馈的动态消息函数本地图片粘贴、拖放和重新打开预览会产生文件名、路径和计数。消息表使用函数格式化例如savingImage(name)、imageTimedOut(path)而不是让调用点写editorLocale ...。imageTimedOut:(path:string)本地图片读取超时${path},savingImage:(name:string)正在保存${name}...,savedImage:(name:string)已保存${name},insertedImage:(name:string)已插入${name}文件名和相对路径属于用户事实不翻译、不更改编码。文本通过 DOM 安全 API呈现不能被当作 HTML。底层 Bridge 仍传固定状态和数据不传本地化后的命令协议。语言改变只影响之后显示的反馈正在进行的资源事务不会取消或重启。预览中的本地图片不可用时现有 image 元素的 title 使用当前词典。读取失败不会因为本地化而泄漏绝对路径原生资产服务只接受受管理的相对路径Web 消息仍受既有边界限制。原生 Bridge 只暴露有限语言入口ArkUI 调用 Web 前先把平台 locale 映射成zh-CN或en再用 JSON 序列化构造受限脚本。Web API 对外暴露setLocale不暴露消息表写入、动态代码执行或文件能力。privatesetEditorLanguage(languageTag:string):void{consteditorLanguageresolveEditorLanguage(languageTag);this.runEditorScript(window.OhMarkdownEditor?.setLocale(${JSON.stringify(editorLanguage)}));}可选链允许 Web 尚未完成加载时安全无操作。之后onEditorReady会再次同步当前语言解决早期调用丢失。原生Watch(language)处理 Ability 配置变化设置点击则主动同步两条路径最终都调用同一个方法。Bridge 不返回用户文档也不让 Web 查询 Preferences。语言选择的事实保留在原生层Web 只接收展示所需的有限标签。这符合最小权限编辑器页面无须知道系统 API 或持久化位置。为什么不能刷新页面页面刷新看似最简单根据语言重新加载 HTML让初始脚本读新值。但它会销毁多个文档 session 的 CodeMirror 状态、撤销历史、光标、滚动、命令面板选择、预览 Object URL、图片读取队列和当前差异。原生层若想恢复需要定义并传输一份庞大快照反而扩大 Bridge 和数据损失面。刷新还会重新解析离线单 HTML、重新初始化 marked 与 DOMPurify、重新绑定事件。大文档下可能出现明显白屏未保存输入若尚未同步原生快照可能直接丢失。语言是轻量表现设置不应该触发文档生命周期。当前方案只更新固定 DOM 和一个 CodeMirror compartment必要时重绘可见的命令或差异。它把成本限制在界面状态而不是正文长度和会话数量上。对 PC 编辑器这种长时间运行、多标签、频繁切换任务的应用这是比“刷新后恢复”更可靠的架构。真实应用截图与运行时证据下面截图来自 HarmonyOS MateBook Pro 2in1 模拟器的 English 模式。原生工具栏、设置面板和状态栏已经切为英文Web 源码编辑区的占位文案同步为Start writing Markdown...证明两套 UI 栈使用同一次用户选择。设备上先切 English强制停止应用并重启英文仍保持选中再切简体中文Web 占位立即变为“开始编写 Markdown…”状态栏显示“界面语言已更新”和“0 字”最后选择跟随系统并重启中文系统下辅助功能树仍返回跟随系统 selectedtrue。截图只能证明可见文案不能证明 CodeMirror 历史未丢。该部分由 Playwright 在同一个页面与 EditorView 中建立内容、切换语言并继续交互来验证。真实设备和浏览器自动化证据互补而不是相互替代。Playwright 覆盖的是状态连续性web-editor/tests/editor.spec.ts中的“运行时切换中英文并同步命令面板与三方比较”用例先调用公开 API 切到中文再检查 HTML lang、编辑器占位、命令面板、中文命令检索和差异区随后切回英文并确认对应文本恢复。测试调用路径与原生相同host.OhMarkdownEditor.setLocale(zh-CN)而不是直接修改 DOM。这样它会经过消息表、命令翻译、Compartment reconfigure 和差异重绘。测试若只写document.title ...无法证明产品入口正确。最终 Playwright30/30同时包含命令面板、三方冲突、图片资产、工作区命令等既有回归。语言功能没有导致原有协议和编辑操作失败。Web TypeScript 检查、Vite 构建与离线单 HTML 生成通过说明词典接口和运行时入口能进入最终产物。设备构建与证据边界ArkWeb 自动化通过后仍需 HAP 层验证因为真实入口来自 ArkUIrunJavaScript平台 Webview 生命周期和资源配置不等同于桌面 Chromium。Debug HAP、ArkTSUnitTestBuild、ohosTest HAP 均通过MateBook Pro 2in1 模拟器 ohosTest 为7/7。最终布局复核产物entry-default-unsigned.hap为 1,520,352 字节SHA-256367ab8650479aa1fa8fe73bd1ebadd9a53f46659c850c2e388fc799d5cb88e5bentry-ohosTest-unsigned.hap为 2,360,824 字节SHA-256b7230037b51044fe16168d2c835fb891e1c70f675941a1046165bc895217592c。两份都是未签名测试包。模拟器证明运行时可见同步和跨重启选择Playwright 证明 Web 内部状态连续ohosTest 证明原生核心测试在设备执行。系统设置改变语言后的完整真机生命周期尚未实测不能从这些结果推导为已经通过。异常处理与安全边界未知语言回退英文缺失命令翻译保留原文本Web 未 Ready 时可选链安全跳过并在 Ready 后重试差异不可见时不重绘动态文件名用 textContent 展示。任何本地化失败都不能改写 Markdown 文档、执行文件命令或关闭会话。setLocale接收字符串但只通过前缀映射到两个内部键不能让调用方选择对象原型属性或注入词典。原生层又先做领域映射和 JSON 序列化形成双重限制。CSP、DOMPurify、文件 URI 保留在 ArkTS 的安全边界都没有因语言功能放宽。词典中的 HTML 特殊字符不会直接进入innerHTML。命令标题和分类由渲染函数以文本写入差异标签也是 textContent。国际化内容本身也应被视为数据而非可信脚本。性能预算与大文档策略语言切换的固定工作包括若干属性赋值、十四条命令翻译、一次 CodeMirror effect 和一次命令列表渲染。三方差异只在可见时重绘。没有遍历 Markdown 正文、没有重建文档树、没有文件 I/O 和网络 I/O。CodeMirror transaction 会触发正常视图更新但占位在非空文档中不产生正文布局成本。大型文档模式下编辑器仍保持 source语言切换不启动预览。命令数量当前很小重新排序和渲染成本稳定。更多语言会增加静态 HTML 体积而非运行时多份实例当前消息表只持有两套小对象。若未来命令、插件和词典增长到明显影响包体应先测量再决定拆分动态加载会破坏离线单 HTML 和增加失败路径不能仅因“国际化架构更高级”而引入。后续扩展必须守住的契约新增语言需要同时提供 EditorMessages 全字段、命令翻译、动态计数函数、HTML lang 映射和 Playwright 切换用例。新增命令需要英文与中文标题、分类和关键词新增 Web 面板需要可见文本与 ARIA 一起接入setLocale。不能在组件内部新增零散editorLocale 分支。繁体中文需要独立术语和测试不能直接映射到zh-CN后宣称支持。复数规则复杂的语言应使用更结构化的格式化层。插件命令若允许第三方提供必须定义缺失翻译降级与安全文本渲染不应让插件覆盖核心命令 ID。状态连续性仍是最高约束任何语言扩展都不能重新创建 EditorView不能清空 Object URL 缓存不能重置活动 session 或 diff 数据。测试必须在切换前建立真实编辑状态切换后继续撤销、搜索和保存才能证明没有隐藏回归。结论OhMarkdown 的 ArkWeb 本地化并非把 DOM 文本集中到一个对象就结束。它通过EditorMessages约束完整性用Compartment对 CodeMirror 做最小重配用稳定命令 ID 隔离翻译与协议用活动比较数据重绘三方差异再由受限 Bridge 接收原生语言标签。整个过程不刷新页面、不读取网络、不触碰用户文档。真实英文应用截图、Playwright30/30、设备 ohosTest7/7和 HAP 哈希共同构成可追溯证据。对优先适配鸿蒙 PC 的 Markdown 编辑器这种运行时连续性尤其重要语言可以改变用户正在编辑的内容、光标和历史必须始终留在原位。