鸿蒙 PC Markdown 编辑器界面语言切换:跟随系统、简体中文与 English 的完整状态闭环

鸿蒙 PC Markdown 编辑器界面语言切换:跟随系统、简体中文与 English 的完整状态闭环

界面语言设置看起来只是三个按钮,真正落到鸿蒙 PC 编辑器中却横跨资源系统、Ability 生命周期、ArkUI 状态、ArkWeb 运行时、持久化和无障碍。任何一段没有接通,用户都会遇到"按钮已切换但部分界面没变""重启后恢复错误""编辑器内容被刷新清空"之类的问题。OhMarkdown 将语言能力作为工作台基础设施处理,而不是把中文字符串散落在组件里。

本文基于公开仓库 https://gitcode.com/VON-/codex_md_oh 的真实实现,核心功能提交为 ed13ee0,布局与设备复核后的当前基线为 0d8d38b。文章只描述已经进入主分支并通过自动化、构建和 MateBook Pro 2in1 模拟器验证的能力:跟随系统、简体中文、English 三种互斥选项,运行时立即更新 ArkUI 与 ArkWeb,并在应用重启后恢复用户选择。繁体中文、日文和云端翻译不在当前实现范围内。

语言设置不是普通的字符串替换

Markdown 编辑器同时运行两套 UI 技术栈。文件树、搜索、设置和状态栏属于 ArkUI;CodeMirror 编辑器、命令面板、预览以及三方差异属于离线 ArkWeb。只更新 ArkUI 资源,编辑器占位文案和命令面板仍会保留旧语言;只更新 Web 文案,系统选择器、工具栏和辅助功能树又不会变化。因此产品层的"当前语言"必须被拆成三个相关但不同的概念:用户选择的模式、当前系统解析出的语言标签、Web 编辑器接受的有限语言标识。

"跟随系统"也不能等同于"当前是中文"。用户选择的是一条持续策略,而不是某次解析结果。若把 zh-Hans-CN 直接写入设置,应用重启后虽然仍显示中文,但设置界面无法知道用户原来选的是"跟随系统"还是"固定简体中文",设备语言后来改成英文也不会跟随。OhMarkdown 因此持久化 default,每次启动和配置更新再解析系统首选语言。

完成标准不是文字变了就结束,而是:三项互斥;选择即时生效;应用内部的 ArkUI 与 ArkWeb 同步;打开文档、光标、撤销历史和脏状态不丢失;重启恢复的是策略;无障碍树能读出选中态;写入失败时仍能给出可理解状态;离线边界不改变。

统一的语言领域模型

entry/src/main/ets/shared/services/LocalizationService.ets 用枚举限制可写入值。SYSTEM 使用平台约定的 default,固定中文和英文使用明确的 BCP 47 风格标签。解析函数对旧值或不同大小写进行归一化,未知值回退到跟随系统,避免损坏的 Preferences 把 UI 留在不可恢复状态。

ts 复制代码
export enum ApplicationLanguage {
  SYSTEM = 'default',
  SIMPLIFIED_CHINESE = 'zh-Hans-CN',
  ENGLISH = 'en-US'
}

export function parseApplicationLanguage(value: string): ApplicationLanguage {
  const normalized = value.toLowerCase();
  if (normalized.startsWith('zh')) {
    return ApplicationLanguage.SIMPLIFIED_CHINESE;
  }
  if (normalized.startsWith('en')) {
    return ApplicationLanguage.ENGLISH;
  }
  return ApplicationLanguage.SYSTEM;
}

这里没有把任意字符串直接交给平台 API。固定集合使设置面板、资源目录、测试矩阵和 Web 文案表保持一致,也给未来增加语言留下明确入口。parseApplicationLanguage 接受 zhzh-CNzh-Hans-CN 等历史形态,但落回领域枚举;它并不声称支持所有中文地区差异,当前产品仍只有一套简体中文资源。

Web 侧不需要理解完整地区标签。resolveEditorLanguage 只映射为 zh-CNen,这是当前 EDITOR_MESSAGES 真正具备的两套词典。把平台标签与 Web 词典键隔开,可以避免 UI 层到处编写 startsWith('zh'),也防止未来增加地区资源时无意访问不存在的 Web 文案。

跟随系统的解析与立即生效

语言应用函数先告诉系统"应用首选语言是什么",再求出当前进程应使用的具体语言。跟随系统时读取首选语言,若平台意外返回空字符串才回退英文;固定语言则直接使用枚举值。最后通过应用上下文更新当前资源配置。

ts 复制代码
export function resolveApplicationLanguageTag(
  language: ApplicationLanguage,
  systemLanguage: string
): string {
  if (language === ApplicationLanguage.SYSTEM) {
    return systemLanguage.length > 0 ? systemLanguage : ApplicationLanguage.ENGLISH;
  }
  return language;
}

export function applyApplicationLanguage(
  context: Context,
  language: ApplicationLanguage
): string {
  i18n.System.setAppPreferredLanguage(language);
  const resolvedLanguage = resolveApplicationLanguageTag(
    language,
    i18n.System.getFirstPreferredLanguage()
  );
  context.getApplicationContext().setLanguage(resolvedLanguage);
  return resolvedLanguage;
}

调用顺序有实际意义。首选语言保存的是用户策略,setLanguage 负责当前进程的资源刷新。若只做后者,当前画面可能改变,但系统和下次启动不知道用户选择;若只做前者,正在显示的页面可能等待下一次生命周期才刷新,用户会误以为按钮失效。

该函数明确可能抛出平台业务异常,调用方必须保留旧状态并报告失败。语言切换不值得通过吞异常制造"界面一半中文一半英文"的假成功。当前实现不把平台错误详情翻译后覆盖,因为原始诊断对开发和设备排查仍有价值。

设置面板的互斥交互

设置入口放在"更多操作"面板顶部,三项用分段式按钮表达互斥关系。它不是三个开关:任何时刻都应有且只有一个值。按钮视觉状态由 applicationLanguage 决定,而不是由解析后的 language 决定,所以中文系统下"跟随系统"和"简体中文"不会同时高亮。

ts 复制代码
@Builder
private applicationLanguageButton(label: Resource, language: ApplicationLanguage) {
  Button(label)
    .type(ButtonType.Normal)
    .layoutWeight(1)
    .height(28)
    .padding({ left: 4, right: 4 })
    .borderRadius(5)
    .fontSize(10)
    .backgroundColor(this.applicationLanguage === language ?
      $r('app.color.workspace_sync_selected') : Color.Transparent)
    .accessibilitySelected(this.applicationLanguage === language)
    .onClick(() => this.updateApplicationLanguage(language))
}

accessibilitySelected 是这段实现中不可省略的一行。只靠背景色不能让屏幕阅读器知道哪个选项生效,也不利于自动化从辅助功能树验证状态。模拟器最终返回"跟随系统 Button selected=true",另外两项为 false,这比截图中的颜色证据更可靠。

按钮高度、字体和内边距使用稳定尺寸,三个长短不同的标签共享一行。中文"跟随系统"和英文 English 都不会改变容器高度,也不会在点击时推动下方设置项。当前设置侧栏允许在 220 至 480 vp 间调整,语言按钮在最小宽度仍保持完整;更长语言名称若在未来加入,则需要重新评估分段控件而不是继续压缩字体。

单次更新必须连接四个状态

WorkspaceShell.updateApplicationLanguage 同时处理平台资源、选中态、@StorageProp 语言、ArkWeb 消息和 Preferences。任何一步失败都不应阻断文档会话。具体顺序是先应用平台语言,获得已解析标签,再更新两个 ArkUI 状态并主动通知 Web,最后异步保存用户策略。

ts 复制代码
private updateApplicationLanguage(language: ApplicationLanguage): void {
  const context = this.getHostContext();
  if (!context) return;
  try {
    const resolvedLanguage = applyApplicationLanguage(context, language);
    this.applicationLanguage = language;
    this.language = resolvedLanguage;
    this.setEditorLanguage(resolvedLanguage);
    saveApplicationLanguage(context, language).catch(() => {
      this.operationStatus = resolveEditorLanguage(this.language) === 'zh-CN' ?
        '无法保存界面语言设置' : 'Unable to save language setting';
    });
    this.operationStatus = resolveEditorLanguage(resolvedLanguage) === 'zh-CN' ?
      '界面语言已更新' : 'Language updated';
  } catch (_) {
    this.operationStatus = resolveEditorLanguage(this.language) === 'zh-CN' ?
      '无法更新界面语言' : 'Unable to update language';
  }
}

这里存在两个失败级别。平台应用失败时进入外层 catch,不宣称语言已更新;Preferences 写入失败发生在画面已经切换后,保留本次运行结果,但状态栏告诉用户无法跨重启保存。这样的语义比"任何保存失败都回滚整个 UI"更稳妥,因为回滚资源配置本身也可能再次失败,并造成编辑器闪烁。

@Watch('onLanguageChanged') 提供生命周期变化的第二入口。当 Ability 因系统配置变化更新 AppStorage,工作台会再次调用 setEditorLanguage。显式点击和系统变化最终收敛到相同的 Web 更新函数,不需要维护两套同步逻辑。

Preferences 保存的是选择而不是结果

语言设置复用 ohmarkdown-settings,没有新增数据库或网络账户。读取时默认值为 ApplicationLanguage.SYSTEM,再经过解析函数;写入后明确 flush,保证模拟器强制停止进程前数据已经落盘。

ts 复制代码
export async function loadApplicationLanguage(context: Context): Promise<ApplicationLanguage> {
  const settings = await preferences.getPreferences(context, 'ohmarkdown-settings');
  const value = await settings.get('application-language', ApplicationLanguage.SYSTEM);
  return parseApplicationLanguage(String(value));
}

export async function saveApplicationLanguage(
  context: Context,
  language: ApplicationLanguage
): Promise<void> {
  const settings = await preferences.getPreferences(context, 'ohmarkdown-settings');
  await settings.put('application-language', language);
  await settings.flush();
}

读取失败回退跟随系统,避免设置存储异常阻止编辑器打开。加载动作在编辑器 Ready 后的一次性初始化中执行;它只恢复分段按钮的策略状态,平台已经在 Ability 启动配置中提供当前资源语言。此处没有把异步加载结果重新强制应用一次,避免启动阶段重复刷新资源。显式选择时才调用平台应用函数。

Preferences 适合这类少量、低频、非敏感的用户设置。它不适合保存整篇 Markdown、撤销历史或大量词典。语言值没有个人隐私,也不需要云同步。产品后续若做跨设备配置同步,必须定义"跟随系统"在不同设备上的语义,不能简单把当前解析语言上传。

Ability 生命周期负责系统变化

EntryAbility 在创建、配置更新和回到前台时把资源管理器中的语言写入 AppStorage。这覆盖了应用启动、系统语言变化通知以及后台期间配置改变三种路径。工作台使用 @StorageProp('language') 订阅,而不是直接持有一次性读取结果。

ts 复制代码
onConfigurationUpdate(newConfig: Configuration): void {
  const colorMode = newConfig.colorMode ?? ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
  AppStorage.setOrCreate('colorMode', colorMode);
  if (newConfig.language) {
    AppStorage.setOrCreate('language', newConfig.language);
  }
}

onForeground(): void {
  const language = this.context.resourceManager.getConfigurationSync().locale ?? 'en-US';
  AppStorage.setOrCreate('language', language);
}

回到前台时再读取一次是必要的防线。设备控制面板可能在应用后台更改语言,配置回调的时序不应成为唯一事实来源。前台同步开销只是一次资源配置读取,不涉及文档重载或目录扫描。

当前模拟器验证了应用内显式切换与强制重启恢复;通过系统控制面板切换设备语言后的完整 PC 真机生命周期仍列为边界。文章不把未执行的真机验证写成已通过。即便如此,Ability 的生命周期路径和跟随系统存储语义已由单元测试与辅助功能树证据覆盖。

ArkWeb 切换不重建编辑器

原生层只执行受限入口 window.OhMarkdownEditor?.setLocale(...)。它不向 Web 暴露 Preferences、系统上下文或任意脚本参数。语言值先被 resolveEditorLanguage 限定为 zh-CNen,再通过 JSON.stringify 进入脚本,避免把未验证字符串拼接到 JavaScript 中。

ts 复制代码
private setEditorLanguage(languageTag: string): void {
  const editorLanguage = resolveEditorLanguage(languageTag);
  this.runEditorScript(
    `window.OhMarkdownEditor?.setLocale(${JSON.stringify(editorLanguage)})`
  );
}

Web 侧不重新加载 index.html,也不重新创建 EditorView。CodeMirror 的文档状态、选择区、历史和滚动均留在原对象中,只有占位扩展通过 Compartment.reconfigure 更新。命令面板若正在打开,会立即重新筛选和渲染;三方差异若正在显示,会基于现有 baseline/local/disk 数据重绘标签,不重新读取文件。

这条设计直接保护编辑器的核心资产。用户可能在未保存文档中完成大量输入,语言设置不能成为清空内容的高风险动作。自动化用运行时中英文切换断言文档内容和视图状态仍存在,而不仅比较某个 DOM 文本。

静态资源与动态状态的边界

ArkUI 静态文案放在 entry/src/main/resources/base/element/string.jsonzh_CN/element/string.json。两套资源键集合必须完全一致,工具栏、文件面板、搜索、设置、冲突操作和空状态都引用 $r('app.string...')。构建验证会解析两个 JSON 并比较键集合,漏翻译不会拖到运行时才发现。

状态栏里部分文本来自已有英文状态机,例如 ReadyModifiedAuto saved。当前工作台在展示层通过 getOperationStatusText 映射常见稳定状态,并保留包含文件名、异常详情或底层服务消息的原文。这样既让正常使用路径本地化,也不会把动态诊断粗暴切割或隐藏。

这一边界还有工程价值:文件处理服务继续返回稳定的技术错误,不需要引用 UI 资源;展示层决定是否翻译。若将来引入结构化错误码,应逐步替代字符串比较,但不能为了"全翻译"一次性改写所有可靠性状态机。语言功能不应扩大文件保存和冲突处理的回归面。

真实应用截图与可见结果

下面截图来自 HarmonyOS MateBook Pro 2in1 模拟器。设置面板显示三段式语言选择,当前选择"跟随系统",系统语言为中文;工具栏、面板标题、状态栏和编辑器占位文案均为中文。

模拟器辅助功能树给出的关键结果为:跟随系统 Button selected=true简体中文 Button selected=falseEnglish Button selected=false。强制停止并重新启动后仍是这一状态,说明持久化的是 default 策略,而非本次解析得到的中文标签。

英文模式另有真实截图和重启验证。原生工作台、Web 占位、命令面板以及三方差异标签同步为英文;切回简体中文后,状态栏显示"界面语言已更新"和"0 字"。两种切换都没有重建标签或改变未保存状态。

自动化测试怎样证明没有假切换

Web Playwright 新增运行时切换覆盖,不只检查页面标题。测试先建立编辑器内容和命令面板状态,再调用 setLocale('zh-CN'),断言中文占位、中文命令搜索、可访问名称和三方比较文本;切回英文后再次断言,同时确认核心文档状态没有被重置。最终 Web 测试为 30/30 通过。

ArkTS 单元测试覆盖三组纯函数:设置值解析、跟随系统标签解析、平台标签到 Web 词典键的映射。资源验证读取 base 与 zh_CN JSON,键集合无差异。Debug HAP、UnitTestBuild 和 ohosTest HAP 均构建通过,MateBook Pro 2in1 模拟器 ohosTest 为 7/7

最终交付 HAP 在布局修复后重新构建,entry-default-unsigned.hap 大小为 1,520,352 字节,SHA-256 为 367ab8650479aa1fa8fe73bd1ebadd9a53f46659c850c2e388fc799d5cb88e5b;ohosTest HAP 为 2,360,824 字节,SHA-256 为 b7230037b51044fe16168d2c835fb891e1c70f675941a1046165bc895217592c。两者都是未签名测试产物,不等同于商店发布包。

异常路径与恢复原则

语言设置需要考虑五类异常。第一,HostContext 不可用,点击直接返回,不发起不完整切换。第二,平台语言 API 抛错,保留当前语言并在当前可读语言中显示失败状态。第三,Preferences 读取失败,策略按钮回退跟随系统,不阻塞文档可靠性初始化。第四,Preferences 写入失败,当前会话仍可使用新语言,但提示无法保存。第五,ArkWeb 尚未 Ready,setEditorLanguage 的调用可能暂时无效,不过 onEditorReady 会再次以当前 language 同步。

Web 词典缺少某条命令翻译时,代码只在存在 messages.commands[command.id] 时覆盖,原始英文仍可作为降级,不会把标题设为空。三方差异没有打开时不做重绘,避免无意义工作。底层文件系统错误维持原始详情,防止翻译造成诊断信息丢失。

这些策略共同遵守一条原则:语言是表现设置,绝不能损坏用户文档。即使本地化链路完全失败,文件打开、编辑、保存和恢复仍应继续工作。当前实现没有增加网络权限、翻译 SDK或第三方动态代码,原有离线安全边界保持不变。

性能与内存影响

ArkUI 资源切换由平台管理;Web 侧更新固定数量的 DOM 属性、命令对象和一个 CodeMirror 扩展,不遍历 Markdown 全文。命令面板重绘的候选数量是固定命令集,三方差异只在其当前可见时重绘。常态切换复杂度与界面文案数量相关,而非文档字符数。

语言词典作为静态对象打入离线单 HTML,不发网络请求。新增中文文本会增加少量 HAP 体积,但不形成长期缓存。切换没有创建第二个 EditorView,不会复制文档缓冲区或撤销历史。对于大文档模式,语言切换仍只更新 UI,避免把预览重新渲染成本混入设置动作。

若未来支持更多语言,词典和资源体积会线性增加。届时可以评估按构建资源机制管理 Web 文案,但在只有中英文时引入动态加载会增加失败点并破坏离线单文件交付,收益不足。

PC 交互与无障碍验收

鸿蒙 PC 用户既可能用鼠标,也可能通过键盘和辅助技术操作。语言控件使用标准 Button 和 selected 状态,点击命中区域稳定;切换后焦点仍在原按钮附近,不会因为页面重建丢到窗口起点。设置侧栏支持拖动,220 vp 最小宽度下三个按钮仍能读取和选择。

辅助功能验收不只看是否存在文本,还看角色与状态。分段选项的 selected 能让测试和屏幕阅读器区分当前值。动态状态栏使用切换后的语言给出成功或失败反馈。Web 的 documentElement.lang、工作区、源码编辑器、预览、命令面板和差异区 aria-label 同步更新,避免视觉是中文而朗读仍是英文。

键盘快捷键不会因语言改变。命令标识如 view.split 保持稳定,只改变标题、分类和搜索关键词,因此原生命令路由和自动化定位不依赖翻译文本。国际化应改变可见表达,不应改变协议、文件格式或快捷键语义。

已知边界与后续演进

当前正式语言只有简体中文和英文。繁体中文不能仅复制简体资源,术语、地区习惯和测试设备都需要独立基线。系统控制面板修改语言后的完整真机生命周期仍需在鸿蒙 PC 真机复核;模拟器已经证明应用内切换、强制停止和重启持久化,但不能替代全部硬件环境。

动态文件名、路径、底层异常和部分复杂状态仍保留原始语言,这是有意的诊断边界,不是宣称百分之百翻译。后续应把稳定状态逐步结构化为错误码与参数,再由资源层格式化,而不是继续扩大英文字符串比较。资源键可以进入自动 CI 校验,防止新功能只补一套语言。

语言选择暂不跨设备同步。若将来做账户配置,需要区分 default 与具体语言,并尊重每台设备的系统语言。隐式把 A 设备解析出的中文同步到 B 设备,会破坏"跟随系统"的本意。

结论

OhMarkdown 的语言功能不是一组翻译文本,而是一条从用户策略到平台资源、从 Ability 生命周期到 ArkUI 状态、再到 ArkWeb 运行时的完整闭环。ed13ee0 提供核心实现,后续布局提交在可调侧栏和窄窗口中继续复核。最终证据表明三项选择互斥、即时生效、跨重启保持,编辑状态不丢失,自动化 30/30、设备 ohosTest 7/7 均通过。

更重要的是,这项能力没有改变文档安全模型:不联网、不重载编辑器、不把任意语言字符串暴露给 Bridge,也不让设置失败阻止编辑。对于优先适配鸿蒙 PC 的 Markdown 工具,这种可预测、可恢复、可验证的基础体验,比简单把按钮翻成中文更接近可长期扩展的产品能力。

相关推荐
FrameNotWork2 小时前
HarmonyOS 6.0 LocalStorage页面级存储
华为·交互·harmonyos
FF2501_940228583 小时前
Grid 构建月历网格:7 列模板 + 日期占位算法
后端·华为·harmonyos·鸿蒙系统
FrameNotWork3 小时前
HarmonyOS 6.0 自定义指令与手势组合:从单指到多指的交互进阶
华为·交互·harmonyos
胡琦博客3 小时前
HarmonyOS 智能工具箱(二):OCR 文字识别工具
华为·ocr·harmonyos
FrameNotWork3 小时前
HarmonyOS 6.0 数据可视化图表
华为·信息可视化·harmonyos
程序员黑豆3 小时前
鸿蒙应用开发:6种图片加载方式详解
前端·华为·harmonyos
qizayaoshuap4 小时前
# ❌ 井字棋 — 鸿蒙ArkTS Minimax AI算法与博弈系统设计
人工智能·算法·华为·harmonyos
Catrice04 小时前
HarmonyOS ArkTS 实战:实现一个校园就业信息与校招应用
华为·harmonyos
math_hongfan5 小时前
鸿蒙Flutter setState机制深入理解
学习·flutter·华为·harmonyos·鸿蒙