第 6 讲 / 共 10 讲

Unicode 之后,还有排版:让多语言文字真正读得通

把数据库改成 Unicode,只解决了文字可以被记录和交换的一部分问题。同一行内容到了屏幕上,还要决定采用什么字形、如何连接、从哪边开始读、在哪儿换行,以及搜索列表按什么顺序排列。多语言界面做得自然,靠的不是把英文标签替换成另一套词,而是尊重这些语言真正的阅读习惯。

Copy2Paste约 11 分钟阅读

编码没有包办阅读体验

设想一张活动报名表:中文姓名、阿拉伯文备注、德语城市和英文订单号同时出现。所有字段都可以包含合法 Unicode,但页面仍可能把备注方向显示错,把姓名从中间截开,或者把城市排在不合预期的位置。编码通过了测试,并不意味着整个用户任务已经通过了测试。

一个便于排查的办法是把流程拆开:先确认解码后的字符序列,再确认字体与字形塑造,之后检查方向、分段、换行和排序。每一层都有自己的输入与输出。屏幕上缺了一个笔画,不一定是传输丢字;复制出来的顺序奇怪,也不一定需要重新编码。先定位层次,往往比换一个字体碰碰运气更快。

团队讨论“支持某种语言”时,也应把范围说具体:能输入和保存,能正确显示,能按习惯搜索,还是经过当地编辑审读?这些都是有价值的能力,却不能彼此替代。一个明确的支持声明能帮助用户判断适用场景,也能帮助维护者制定下一步工作。

字形塑造把字符变成可读形态

字形塑造,也常称 shaping,是依据文字、字体和上下文选择并定位字形的过程。HarfBuzz 这类引擎为这一阶段提供能力。阿拉伯文中的连接形态以及一些南亚文字的组合,说明字符与最终字形之间并非简单的一一映射。一个字体文件里有某些字符,也不保证手工逐字绘制就能得到正确结果。

对网页作者来说,最实际的建议是让成熟的文本系统处理整段文字。为了让每个字符依次跳动,把一段文本按码点切成彼此独立的元素,可能破坏塑造所需的上下文。动画可以很漂亮,但若设计以拆散语言为代价,就需要调整实现方式,例如对更大的文本片段做整体运动。

字体回退同样需要实测。某个罕见字符能够显示出来,仍可能与邻字的基线、粗细和行高不协调。选择字体时应带上真实段落,而不只是单字样张。读者关注的是完整句子是否舒适、文字是否准确,字体清单里多了几个名称并不能回答这些问题。

阅读方向与存储顺序不是一回事

阿拉伯文、希伯来文与英文编号混排时,页面需要处理双向文本。Unicode 双向算法描述如何从逻辑顺序推导显示顺序;程序一般应保存文本的逻辑顺序,让显示系统安排视觉位置。把字符串倒过来,不能替代双向排版,反而会让搜索、复制和编辑继续出错。

尤其容易暴露问题的是标点与数字。假设一条从右向左阅读的消息末尾包含订单号和括号,标点应属于哪个片段,需要上下文才能判断。测试如果只放一行纯阿拉伯字母,可能看不见这些缺陷。加入数字、冒号、括号和拉丁用户名,才能接近真实通知或聊天消息。

方向问题也不能通过整页右对齐解决。右对齐决定行放在容器哪一边,文本方向影响内部顺序,两者职责不同。界面结构可以从右向左排列,嵌入其中的产品型号仍按自身方向阅读。准确区分这两个概念,能让样式修改更小,也更容易验证。

把动态插入的名字隔离成一个单元

在网页里展示用户输入的名字、商品型号或搜索词时,可以借助 bdi 或带有合适 dir 属性的结构,隔离一段文本的方向影响。对未知方向内容,dir="auto" 是可考虑的选择。具体标记应结合上下文使用,而不是在所有字符串前后塞入同一组不可见控制字符。

例如“评论者:某个名字(管理员)”包含固定文案和动态名字。如果名字的方向影响了外面的括号,读者就可能把角色标识归给错误片段。把动态名字作为一个独立单元,可以让其内部方向由自身决定,同时减少对相邻文案的干扰。验收时应连同前后标点一起观察。

复制行为需要另行检查。网页标记帮助浏览器显示,纯文本剪贴板没有同样的元素边界。一个页面看起来正确,不等于粘贴到每个目的地都同样显示。工具应保留用户内容,并针对真正的使用场景测试;不要为了修好一次截图,永久改写所有存储字段。

html
<p lang="ar" dir="rtl">
  الطلب: <bdi dir="ltr">AB-120</bdi>
</p>
<p>评论者:<bdi lang="ar" dir="auto">اسم المستخدم</bdi></p>

语言标记提供了必要的上下文

HTML 的 lang 属性告诉浏览器和辅助技术内容使用什么语言。整个中文页面可以声明 zh,英文引文或其他语言片段则按实际内容补充相应标记。语言和方向是不同信息:lang="ar" 不应被当成 dir="rtl" 的代用品,反过来也是如此。两者都需要按实际内容表达。

中文、日文和韩文会使用共享的汉字编码,但这不意味着它们应该显示完全相同的地方字形。语言标记与合适字体可以为渲染提供上下文;最终是否选择到预期字形,还取决于字体和平台。多语言页面应准备当地读者熟悉的样本,不能只验证“没有方框”就结束。

语言标记还不能替代编辑。英语按钮常用简洁的动作词,中文操作提示也应采用自然的动宾结构,而不是机械保持原文词数。技术标记告诉系统如何处理内容,语言编辑决定内容是否像当地人会说的话。二者共同作用,页面才不会显得只是换了一层文字。

词边界不能只靠空格寻找

按空格拆分英文句子,是许多入门示例的起点。但中文、日文和泰文等内容并不总在词与词之间使用空格。把同一段算法用于所有语言,可能把整句都计为一个词,也可能把标点、组合符号和表情错误地当成独立可编辑单位。先明确要找词、句子还是字素簇,才能选择合适的分段方法。

下面的 JavaScript 示例请求中文词分段,并只保留被标记为词的片段。它展示了如何调用语言相关能力,却不承诺所有运行时返回永远相同的词表。分词依赖实现和数据,搜索引擎还可能加入自己的词典。若某个任务需要稳定复现,应记录运行环境,并为关键例子设定期望行为。

文章阅读时长也要据此解释。简单的空格词数适合某些英文估算,用到中文时却可能明显失真。产品可以分别使用适合语言的估算方法,或者明确显示按字符计算的粗略结果。计数是帮助用户理解内容的工具,不应该因为实现方便而伪装成跨语言的精确尺度。

javascript
const segmenter = new Intl.Segmenter("zh", {
  granularity: "word"
});
const words = [...segmenter.segment("我们一起学习文字排版。") ]
  .filter(item => item.isWordLike)
  .map(item => item.segment);
console.log(words); // Segmentation can vary by runtime data.

中文换行还要照顾标点

换行算法寻找的是允许断开的机会,浏览器还要结合容器宽度、字体度量和排版规则决定真正在哪儿换行。中文没有大量空格,并不意味着可以在任意位置生硬切开。例如句末标点孤零零站到下一行开头,或者左括号悬在上一行末尾,都可能破坏阅读节奏。

中文排版需求文档详细讨论了标点、禁则和中西文混排等问题。实现页面时,可以把这些原则转成具体样本:长标题里放书名号和括号,正文里放数字单位与英文缩写,在窄屏和放大字体之后重新观察。只用一段没有标点的占位文字,几乎不会触发真正困难的情况。

不要用手工插空格或硬换行掩盖某个屏幕宽度下的缺陷。那些字符会进入搜索、复制和朗读内容,并在另一种宽度下变成新的问题。文章应保留自然文本,布局交给合适的 CSS 与排版系统;只有作者真正希望段落或诗行在此结束时,才写入结构性的换行。

字典顺序是语言约定

按码点排序很容易复现,但它不等于每种语言的字典顺序。语言相关排序,也叫 collation,会根据语言和选项处理字母、重音等差别。本篇用同一组字符串 ä、z、a 在德语和瑞典语排序,得到不同顺序,这并不是其中一个语言环境出了错。

在 JavaScript 里,可以创建带明确 locale 的 Intl.Collator,并使用它的 compare 函数。对于给人浏览的名单,选择符合读者习惯的排序通常更合理;对于导出机器处理的稳定集合,则可能需要明确的码点顺序。重要的是在功能名称和说明里说清采用哪一种,而不是把所有列表都标成笼统的“升序”。

排序还需要处理比较结果相等的情况。如果当前选项把某些差异忽略了,就可以保留原始顺序,或使用公开说明的第二排序键。不要让页面每次刷新都以偶然的数据库返回顺序改变位置。稳定的结果能帮助用户建立位置记忆,也方便报告真正的排序问题。

javascript
const names = ["ä", "z", "a"];
console.log([...names].sort(new Intl.Collator("de").compare));
// ["a", "ä", "z"]
console.log([...names].sort(new Intl.Collator("sv").compare));
// ["a", "z", "ä"]

输入过程也是多语言体验的一部分

中文输入法常先产生正在组合的内容,之后才确认最终文字。W3C 的 UI Events 草案用 compositionstart、compositionupdate 和 compositionend 描述组合会话的阶段。如果应用在每次按键之后立即改写输入值、移动光标或执行提交,可能打断这个过程。验收表单时,应真正用输入法选词,而不是只把最终中文字符串粘进去。粘贴能验证存储,却覆盖不到候选词选择时的交互。

同样,删除、移动光标和选择文本都应尊重用户感知的边界。一个由多个码点组成的可见单位,不应该在普通计数器里被随意拆开。复杂编辑器可能需要更细的操作规则,但这些规则应该是主动设计的结果,而不是沿用单字节字符串思路留下的偶然行为。

可以让本地语言编辑参与一组具体任务:输入自己的语言、修改一句话、复制一个含标点的片段、查找并排序一份名单。请编辑描述在哪里犹豫、哪里需要重新操作。这些观察比只问“翻译对不对”更能揭示产品是否真正适合当地使用。

用真实任务建立一套小型验收样本

一套实用的样本可以包含中文标点与英文编号、带重音的人名、阿拉伯文加括号、泰文短句和一个复合 Emoji。让每个样本经过输入、保存、重载、搜索、复制和导出,再分别核对原始内容与视觉结果。文字是否丢失、顺序是否变化、显示是否自然,是三种不同的结论。

样本库最好由技术人员与当地编辑共同维护。技术人员记录环境和可复现步骤,编辑解释某种显示为什么别扭,以及哪种表达更自然。发现问题后,把真正暴露缺陷的最短案例留下来,避免不断扩大一份没人会认真执行的检查清单。每次变更只补充有依据的回归场景。

Unicode 提供共享的字符基础,让不同文字进入同一套数字系统。一个好的多语言产品还需要在此基础上完成字形、方向、语言和交互的工作。读者不必知道这些步骤的名字;他们应该能够顺畅写出自己的语言,读懂内容,并把同一份文字可靠地带到下一个地方。

参考资料

  1. Unicode Standard Annex #9: Bidirectional Algorithm ↗
  2. W3C: Inline Markup and Bidirectional Text ↗
  3. W3C: Declaring Language in HTML ↗
  4. HarfBuzz: What Is HarfBuzz? ↗
  5. Unicode Standard Annex #29: Text Segmentation ↗
  6. Unicode Standard Annex #14: Line Breaking Algorithm ↗
  7. W3C: Requirements for Chinese Text Layout ↗
  8. ICU: Collation Concepts ↗
  9. ECMA-402: Intl.Segmenter ↗
  10. W3C UI Events: Composition Events (Working Draft) ↗