准备应用市场文案时,一行看上去没有问题的文字,在两个编辑器中却可能排出不同顺序:某个词被隐藏的双向控制字符重新排列,或者英文品牌名被零宽字符拆开,人工肉眼很难发现。对多语言产品而言,出现异常时最麻烦的不只是显示变样,而是测试人员难以说明“哪一个文件的哪一个不可见字符导致了差异”。

这次选择写一个不接入上架后台的离线门禁。GlyphFence读取本地多语JSON,标出需要人工复核的Unicode控制符,给出清晰的码点和状态。它不是华为官方审核规则,也不能根据扫描通过就声称应用必然审核通过;它只是在提交资料之前把肉眼不容易看到的输入转换为可讨论、可回归的检查结果。

一、先决定检查对象,而不是先删除字符

示例项目名为GlyphFence,检查编号为PUB-2909。示例有三份审核展示文案:zh-CN内容“安全相册”,预期PASS;en-US内容在Photo与Vault之间夹有U+200B零宽空格,预期ZERO_WIDTH;ar-SA内容在阿拉伯语词组中出现U+202E右向左覆盖符,预期BIDI_CONTROL。聚合结果是1/3 PASS · BLOCKED。

这些例子有一个很重要的限制:检测到字符不等于字符有害。阿拉伯语、波斯语等文字本身有从右向左书写的合理需求,部分双向标记在跨脚本排版里也有合法用途;零宽连接符U+200D还可能参与某些复杂文字或表情序列的形成。工具只把风险位置报告出来,产品团队应结合语言、文案来源和排版目标决定是保留、替换还是登记白名单。本文示例使用比较严格的本地策略:存在未核准的控制符就阻断本次内部交付。

此处把BLOCKED定义为团队CI里的待复核状态,不代表应用市场直接拒审。也不要采用“正则一匹配,就把所有不可见字符清空”的做法,那很可能破坏原文语义。真正需要的是可解释的扫描结果、稳定的数据源和复核记录,而不是一个看似万能的清洗按钮。

二、为什么普通长度统计会漏掉问题

JavaScript字符串以UTF-16代码单元表示,开发时常用的length无法表达用户肉眼看到的字形数量。对于BMP范围里的U+200B和U+202E,长度统计可能出现额外单位,但如果没有基准文本,检查人员根本不知道为什么多了一位。另一类字符处于代理对表示的补充平面,单纯逐个索引甚至会误切码点。

因此这个工具分两步。第一步使用正则找出需要关注的Unicode范围,第二步使用for...of遍历码点,再把字符转成U+XXXX形式的证据。将扫描结果结构化后,才考虑是否拒绝构建。下面不是HarmonyOS系统API,而是在开发电脑上运行的普通Node.js脚本,使用的RegExp、codePointAt都属于JavaScript标准能力。

const BIDI_CONTROL = /[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/u;
const ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/u;

function classify(value) {
  if (BIDI_CONTROL.test(value)) return 'BIDI_CONTROL';
  if (ZERO_WIDTH.test(value)) return 'ZERO_WIDTH';
  return 'PASS';
}

function suspiciousPoints(value) {
  const points = [];
  let position = 0;
  for (const char of value) {
    if (BIDI_CONTROL.test(char) || ZERO_WIDTH.test(char)) {
      points.push({ index: position, codePoint:
        `U+${char.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}` });
    }
    position += char.length;
  }
  return points;
}

classify按双向控制优先、零宽其次返回状态,一个字段如包含两类字符,它会先显示BIDI_CONTROL。suspiciousPoints仍会输出全部命中的码点,避免优先级掩盖次级问题。这里index表示UTF-16代码单元索引,便于回到JavaScript源字符串定位;它不是用户感知字符位置。在报告中应明确该定义,后续需要用户可见的字素簇坐标时,再引入相应的分段逻辑。

正则覆盖的是审计者选定的一组字符,并不声称涵盖所有Unicode安全问题。字符形似、组合附加符、规范化形式及不同字体下的视觉欺骗都不在这个小脚本的保证范围内。若业务强调品牌保护,还需要设计独立的混淆字形审查流程。

三、清单要能追溯到具体语言文件

在多语言工程里,文案往往分散于许多资源文件。示例把待发布文案导出为review-copy.json,结构为语言到展示文案的映射。导出动作不在本文范围内,脚本只审这个明确的中间清单,这样既能在CI独立运行,也不会对HarmonyOS资源目录格式做未经核实的假设。读取数据后,逐语言计算状态和风险码点,最后设置进程退出码。

import { readFile } from 'node:fs/promises';

async function auditManifest(filePath) {
  const raw = await readFile(filePath, 'utf8');
  const source = JSON.parse(raw);
  if (source === null || Array.isArray(source) || typeof source !== 'object') {
    throw new Error('MANIFEST_NOT_OBJECT');
  }
  const rows = Object.entries(source).map(([locale, value]) => {
    if (typeof value !== 'string') throw new Error(`BAD_VALUE:${locale}`);
    return { locale, status: classify(value), points: suspiciousPoints(value) };
  });
  const pass = rows.filter(row => row.status === 'PASS').length;
  const summary = pass === rows.length ? 'PASS' : 'BLOCKED';
  return { rows, pass, total: rows.length, summary };
}

auditManifest先检查根节点类型,再检查每条文案必须是字符串。没有必要让空值悄悄变成字符串“undefined”,否则真实丢失的文案可能被误判为PASS。当目录读取失败、JSON结构损坏或编码异常时,应与“文案含不可见字符”区分为不同错误,CI不能把文件加载失败计入通过数量。

这里返回的rows适合输出为JSON检测报告,也可以渲染在一个HarmonyOS检查结果页面中。

四、用显式转义样本防止案例本身不可见

将不可见控制符直接复制到代码块,会让读者在不同平台反复遇到“看不见但删不掉”的文字。因此样本使用Unicode转义来写出问题位置。下面给出可独立执行的Node.js入口;它与前两个代码块的函数组成同一个.mjs文件时,运行结果由输入内容决定,而不是事先写死的状态文案。

const sample = {
  'zh-CN': '安全相册',
  'en-US': 'Photo\u200BVault',
  'ar-SA': '\u062A\u0637\u0628\u064A\u0642 \u202E\u0635\u0648\u0631'
};

const rows = Object.entries(sample).map(([locale, value]) => ({
  locale, status: classify(value), points: suspiciousPoints(value)
}));
for (const row of rows) {
  console.log(`${row.locale} ${row.status} ${JSON.stringify(row.points)}`);
}
const passed = rows.filter(row => row.status === 'PASS').length;
console.log(`PUB-2909 ${passed}/${rows.length} PASS · ${passed === rows.length ? 'PASS' : 'BLOCKED'}`);

三个语言的预期状态依次为zh-CN PASS、en-US ZERO_WIDTH与ar-SA BIDI_CONTROL;合计PUB-2909 1/3 PASS · BLOCKED。风险码点应分别出现U+200B与U+202E。这套样本不需要网络、HarmonyOS账号或应用市场后台,因此适合写进代码仓库的单元测试。正式走auditManifest文件入口时,应把读取失败与文案风险分别转换为明确退出码,并保留生成的检测报告供人工复核。

示例里按语言分组显示卡片,绿色表示没有命中已配置的控制符,黄色表示需要核对字符用途。选择en-US应能看到U+200B,选择ar-SA应能看到U+202E。报告中不要直接打印包含潜在双向控制符的原始文案作为唯一证据,因为终端本身也可能被重新排版;建议同时打印安全转义后的片段和字符索引。

五、怎样建立不伤害合法文字的白名单

CI门禁最容易走向两个极端:要么扫描到任何不可见字符就机械删除,要么把所有阿拉伯语或表情符号都放行。更稳妥的策略是基于字段和码点共同判定,给每条允许记录注明语言、资源键、具体码点、允许原因、审查人及到期复核时间。只有符合全部上下文的命中项才进入白名单,不能因为ar-SA有合法RTL需求,就把该语言中任何RLO都视作安全。

如果文案来自翻译平台,最好保留源文与导出物的版本关系,并在提交前比较文件哈希。哈希能告诉团队文件有没有变化,却解释不了变化是不是安全;本工具的码点报告正好补上这一层。这里与以往的多语言哈希校验不是一回事:哈希解决“文件是否同版”,当前方法解决“文件里是否存在需要语义复核的隐藏控制字符”。

还要考虑自然语言测试的环境差异:不同终端字体、渲染引擎和输入法可能处理双向标记的方式不同。仅靠扫描器不会检测真实页面里的行内混排效果。建议把被标记字段交给熟悉对应语言的审校人员,结合实际UI截图复核,并在必要时对屏幕阅读器的朗读顺序做单独测试。

最终示例手机画布保留PUB-2909、1/3 PASS与三条语言状态,所有数值都来自固定样本,可由读者在本地Node.js中验证。

六、限定工具职责,才能让结果可信

华为应用市场公开流程会要求开发者填写应用名称、介绍、截图、语言以及其他提交信息,这解释了为什么多语言文案质量值得在发布前检查。但这些公开资料没有规定本文选定的Unicode码点必然触发审核拒绝,所以工具里的BLOCKED只是团队自行设定的预检门槛,不能写成“官方自动驳回”。官方流程也可能更新,提交前应以当时平台后台要求为准。

工程上还可以扩展误报统计、语言级别白名单、检测报告比对和编辑器定位链接,但不要在没有审校资源时直接自动删除字符。对翻译团队来说,一份能指出“第几条语言、哪个代码点、为什么被标注”的报告,比一个难以复现的红色失败提示更实用。

这个小工具所做的事情很窄:对已经导出的文案清单扫描指定不可见控制符,并阻止未经复核的样本继续被交付。它不证明文字合规、翻译准确、审核必过,也不负责设备上的真实渲染。把边界写清楚,反而能让它成为可靠的流水线环节。

参考资料:华为应用市场发布与审核流程、HarmonyOS ArkTS开发语言与生态说明。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐