双向语序
December 07, 2024
什么是双向语序?
双向语序(Bidirectional Text)是指在同一文本段落中混合存在两种书写方向的现象:
- RTL(Right-to-Left):从右向左书写的语言(如阿拉伯语、希伯来语)
- LTR(Left-to-Right):从左向右书写的语言(如英语、中文)
当这两种方向的文字混合使用时,会出现复杂的文本方向嵌套问题。例如:
اللغة العربية (Arabic) 和 English 混合显示
1. Unicode双向算法(UBA)
Unicode标准通过逻辑顺序存储+视觉顺序渲染的机制实现双向支持:
- 逻辑顺序:按字符输入顺序存储(内存中的二进制序列)
- 视觉顺序:根据算法动态计算展示顺序(屏幕上的显示效果)
2. 字符类型分类
Unicode将字符分为一下四类,决定其在双向文本中的行为:
字符类型 | 算法逻辑 | 示例 | Unicode 官方说明 |
---|---|---|---|
强字符 | 强字符自带明确的 LTR 或 RTL 属性,不受上下问干扰 | Hello 你好 مرحباً - | Strong characters are those with a definite direction. Examples of this type of character include most alphabetic characters, syllabic characters, Han ideographs, non-European or non-Arabic digits, and punctuation characters that are specific to only those scripts. |
弱字符 | 弱字符会跟随上下文内容决定逻辑语序为RTL或LTR。在混排场景下容易出现语序展示异常问题 | +-×÷ ¥$€ 1234567890 | Weak characters are those with vague direction. Examples of this type of character include European digits, Eastern Arabic-Indic digits, arithmetic symbols, and currency symbols. |
中性字符 | 中性字符没有方向属性 | (tab) (空格): , . | 。 | Neutral characters have direction indeterminable without context. Examples include paragraph separators, tabs, and most other whitespace characters. Punctuation symbols that are common to many scripts, such as the colon, comma, full-stop, and the no-break-space also fall within this category. |
显式格式化 | 又称“假强字符”,在文本展示中没有符号显式,用户不可见,但是会占用2个字符长度。 可以用来修正弱字符的展示顺序。 |
“\u202A \u202C” | Explicit formatting characters, also referred to as “directional formatting characters”, are special Unicode sequences that direct the algorithm to modify its default behavior. These characters are subdivided into “marks”, “embeddings”, “isolates”, and “overrides”. Their effects continue until the occurrence of either a paragraph separator, or a “pop” character. |
3. 方向控制标记
一言蔽之:本质是通过Unicode符号显式分隔字符标记起始点,对字符的书写方向进行定义,使文本内容始终按照标记的方向进行展示,而不受段落上下文影响。
// 示例:包裹LTR文本使其在RTL环境中保持方向
const address = "\u202A15 Bay Street, Laurel, CA\u202C";
Unicode符号表
名称 | 方向 | Unicode Code | HTML Code | 模式 |
---|---|---|---|---|
Left-To-Right Mark(LRM) | 左->右 | U+200E | (实体是) | 隐性 |
Right-To-Left Mark(RLM) | 右->左 | U+200F | (实体是) | 隐性 |
Left-To-Right Embedding(LRE) | 左->右 | U+202A | ordir =“ltr” | 显性 |
Right-To-Left Embedding(RLE) | 右->左 | U+202B | ordir =“rtl” | 显性 |
Left-To-Right Override(LRO) | 左->右 | U+202D | or | 显性 |
Right-To-Left Override(RLO) | 右->左 | U+202E | or | 显性 |
Left-To-Right Isolate(LRI) | 左->右 | U+2066 | or | 显性 |
Right-To-Left Isolate(RLI) | 右->左 | U+2067 | or | 显性 |
First Strong Isolate(FSI) | U+2068 | ordir =“auto” | 显性 | |
Pop Directional Formatting(PDF) | 结束标记 | U+202C | or | 显性 |
Pop Directional Isolate(PDI) | 结束标记 | U+2069 | or | 显性 |
## 常见问题与解决方案 |
1. 文字顺序混乱
典型场景:
原文:"15 Bay Street" 插入阿拉伯语段落
错误显示:"Street Bay 15"
根本原因:
- 数字(弱字符)与字母(强字符)混合
- 中性空格符继承主文方向
解决方案:
// 使用LRE+PDF包裹整个地址
const fixedAddress = "\u202A15 Bay Street\u202C";
2. 嵌套方向错乱
典型场景:
英文引用阿拉伯语句子中的英文术语
错误表现: Parent (RTL) -> Child (LTR) -> Grandchild (RTL) 嵌套失败
解决方案:
<!-- 使用隔离标记建立独立方向上下文 -->
<span dir="rtl">
父文本
<bdi dir="ltr">子文本 <bdi dir="rtl">孙文本</bdi></bdi>
</span>
3. 标点符号错位
典型场景:
阿拉伯语句尾的英文问号显示在左侧:"ماذا حدث?"
正确显示应为:"ماذا حدث؟"
解决方案:
- 使用语言专属标点(阿拉伯语问号؟)
- 添加RLM控制符:
const fixedText = "ماذا حدث" + "\u200F?\u200F";
4. 数字显示异常
典型场景: 阿拉伯语中的欧洲数字”123”显示为”321”
解决方案:
// 使用阿拉伯-印度数字(٠١٢٣٤٥٦٧٨٩)
const arabicNumbers = "١٢٣";
// 或强制指定方向
const fixedNumbers = "\u202A123\u202C";
各技术栈能力
技术栈 | API | 方案说明 | 备注 |
---|---|---|---|
Web | unicode-bidi | CSS unicode-bidi 属性,和 direction 属性,决定如何处理文档中的双书写方向文本(bidirectional text)。比如,如果一块内容同时包含有从左到右书写和从右到左书写的文本,那么用户代理(the user-agent)会使用复杂的 Unicode 算法来决定如何显示文本。unicode-bidi 属性会覆盖此算法,允许开发人员控制文本嵌入(text embedding)。 unicode-bidi 与 direction 是仅有的两个不受 all 简写影响的属性。 <br>/* 关键字值 */<br>unicode-bidi: normal;<br>unicode-bidi: embed;<br>unicode-bidi: isolate;<br>unicode-bidi: bidi-override;<br>unicode-bidi: isolate-override;<br>unicode-bidi: plaintext;<br>/* 全局值 */<br>unicode-bidi: inherit;<br>unicode-bidi: initial;<br>unicode-bidi: unset;<br> |
默认情况下,浏览器会通过RTL标签自动识别文字和布局方向。 对于识别异常的场景,需要通过特殊处理来进行修正。 |
Java | Bidi BreakIterator |
- Bidi 是Java ICU工具包提供的能力,基于 Unicode Standard Annex #9. 算法封装。 - BreakIterator 用于设置字符边界 |
Java标准ICU工具包 |
iOS | NSString | iOS:SupportingRight-To-LeftLanguages setbasewritingdirection:文字方向为基础能力,无法彻底解决混排问题。 Unicode标记 <br>// Wrap the plus (+) prefix and phone number in left-to-right directional markers<br>NSString *phoneNumber = @"408-555-1212";<br>NSString *localizedPhoneNumber = [NSString stringWithFormat:@"\u202A%@\u202C", phoneNumber];<br> |
手动控制 |
Android | BidiFormatter | 根据文本中强字符的方向,为非强字符添加方向属性,避免文本占位嵌入时展示错乱。 如:LTR文本中包含了数字、空格等非强字符,在将这段话插入到RTL文本中时,可能会导致LTR文本中的非强字符被上下文RTL强字符影响,导致展示错乱。 |
基于ICU能力封装 |
参考
- https://en.wikipedia.org/wiki/Bidirectional_text
- https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/dir
- https://developer.mozilla.org/zh-CN/docs/Web/CSS/unicode-bidi
- https://www.etymonline.com/word/bidirectional#etymonline_v_27087
- https://leancode.co/blog/right-to-left-in-react
- https://www.w3.org/International/articles/inline-bidi-markup/uba-basics
- https://www.w3.org/International/articles/strings-and-bidi/
- https://www.npmjs.com/package/bidi-js
阅读量
Written by xi ming You should follow him on Github