【Web国际化】漫谈阿拉伯语情境下的RTL布局适配
人类并不是活在国家之中,而是活在语言之中。[母语]才是我们的[祖国] ——罗马尼亚思想家 《合金装备V》
从文字方向说起:BiDi
在中国古代,文人墨客在书写文字时,秉承着从上到下,从右到左的的顺序。据说是为了不让衣袖弄脏未干的墨水。
目前,我们接触的绝大部分语言,都是从左至右书写的(LTR,left to right)。
事实上,仍然有不少语言是从右往左书写(RTL,right to left),例如:希伯来语、阿拉伯语。
在计算机领域,为了兼容这两种布局模式,文字的显示顺序与内存中的字符顺序并非是固定的。
Unicode 定义了它其中每个字符的方向属性,由Unicode双向算法( Bidirectional Algorithm ,简称Bidi)在显示时产生正确的顺序。。
双向算法,顾名思义,只有当两种不同方向的文本混合排列时,才会发挥作用。
文字方向原理与Unicode控制字符
Unicode方向属性包含以下三种类型:
强字符:大部分的字符都属于强字符,比如英文字母、汉字和阿拉伯字母。它们的方向性是确定的(英文字母是从左到右,而阿拉伯字母则从右到左),和其上下文的bidi属性无关。并且**,强字符在bidi算法中可能会影响其前后字符的方向性**;
弱字符:数字和数字相关的符号属于弱字符。它们的方向性是确定的,但对其前后字符的方向性不会产生影响;
中性字符:大部分的标点符号和空格属于中性字符。它们的方向性是不确定的,由上下文的bidi属性来决定其方向。
小知识:阿拉伯数字
很多人都以为阿拉伯数字是阿拉伯发明的,其实是由印度人发明的。
然后被阿拉伯地区引进并且广泛传播到西方世界,因此才被叫做阿拉伯数字,真正阿拉伯地区使用的数字是叫做阿拉伯文数字,那边使用两套数字
而且很有意思的是:同阿拉伯文不一样,当代的阿拉伯文数字一般从左向右排列.而在一些古典作品里是从右向左排列的.
全局方向
全局方向是一个区域中的总体方向。中英文环境一般是 (LTR) 从左至右,而阿拉伯文环境则为 (RTL) 右至左的书写顺序。我们可以通过dir属性或者direction样式,设置指定方向。
当指定HTML标签的dir属性之后,其他所有常规子元素的dir属性将会默认继承HTML标签的dir属性。当然,也可以手动更改,或者在css中设置direction进行更改
<style>
html {
direction: 'rtl'
}
</style>
<html dir="rtl"></html>
已显示电话号码为例:
<!-- ⚠️Do Not Use,错误的使用方式,显示的方向并不正确 -->
<p dir="rtl">هاتف:(415)555-3695</p>
这段HTML代码在浏览器会被渲染成下图所示的内容:
如何兼容: <bdi>、dir和direction
浏览器提供了bdi元素用于显示未知方向的文字。
HTML 双向隔离元素( ) 告诉浏览器的双向算法将其包含的文本与周围的文本隔离,当网站动态插入一些文本且不知道所插入文本的方向性时,此功能特别有用。 -MDN
同时,大多数HTML元素都提供了dir="auto"属性,可以自动设置文字显示方向。
<!-- 以下两种方式等价 -->
<div dir="auto">This text has been displayed in LTR</div>
<div><bdi>This text has been displayed in LTR</bdi></div>
但是,经过研究,dir="auto"和bdi这两种选择方向文字的算法,都是通过判断第一个强方向字符的方向,在一些边缘case下,可能会出现问题,例如:
<!-- Hello 我是一名前端开发 -->
<bdi>Hello أنا مطور الواجهة الأمامية</bdi>
<!-- 很明显,这段文字应该以RTL显示,但由于是单词开头,导致整段文本以LTR显示,造成语序错误 -->
显示效果,注意本来应该从右往左阅读:
更优的方案
为了解决bdi和dir="auto"的误判问题,我们需要选择更优的算法
人类最古老而又最强烈的情感是恐惧,而最古老又最强烈的恐惧是未知。 -克苏鲁神话
Flutter 是如何判断文字方向的
Flutter提供了Bidi包用于判断文字方向,由于Flutter开源,我们很容易就可以看到其判断文字方向的逻辑:
https://github.com/dart-lang/intl/blob/master/lib/src/intl/bidi.dart
核心逻辑在256行的estimateDirectionOfText函数,代码很清晰,注释也很清楚,简单解释就是如果是URL或者有数字,则强制以LTR显示。
否则,用空格分隔字符元素,已RTL文字元素开头占比超过总元素占比的0.40(_RTL_DETECTION_THRESHOLD),则认为整端文本已RTL显示。
至于_RTL_DETECTION_THRESHOLD为何取0.4,代码中并没有给出原因。个人猜测是一个经验值。
/// Constant to define the threshold of RTL directionality.
static const _RTL_DETECTION_THRESHOLD = 0.40;
static TextDirection estimateDirectionOfText(String text,
{bool isHtml = false}) {
text = isHtml ? stripHtmlIfNeeded(text) : text;
var rtlCount = 0;
var total = 0;
var hasWeaklyLtr = false;
// Split a string into 'words' for directionality estimation based on
// relative word counts.
for (var token in text.split(RegExp(r'\s+'))) {
if (startsWithRtl(token)) {
rtlCount++;
total++;
} else if (RegExp(r'^http://').hasMatch(token)) {
// Checked if token looks like something that must always be LTR even in
// RTL text, such as a URL.
hasWeaklyLtr = true;
} else if (hasAnyLtr(token)) {
total++;
} else if (RegExp(r'\d').hasMatch(token)) {
// Checked if token contains any numerals.
hasWeaklyLtr = true;
}
}
if (total == 0) {
return hasWeaklyLtr ? TextDirection.LTR : TextDirection.UNKNOWN;
} else if (rtlCount > _RTL_DETECTION_THRESHOLD * total) {
return TextDirection.RTL;
} else {
return TextDirection.LTR;
}
}
Google Doc根据测试,好像也用的是相似的算法,由于代码做了混淆,这里并没有去深入研究。
最佳实践
在阿拉伯语的环境下,需要格外小心中性字符和英文字符
一般来说,如果用户当前设置的语言是LTR语言(英语、中文等),页面中是不太可能出现RTL语言的。
但是,当用户当前设置的语言是RTL语言时,则需要格外注意。由于阿拉伯语数字和英文的广泛使用,如果不加以区分,很容易出现排版错误。
尽可能对电话号码、email地址,使用单独的标签进行包裹,并强制其direction为ltr。例如上图出现的“电话号码”例子:
<!-- 正确的排版方向 -->
<p dir="rtl"><span>هاتف:</span><span dir="ltr">(415)555-3695</span></p>
一个更加详细的例子:
用户输入的内容,不要继承默认的布局方向
应该使用使用<bdi>、或dir=auto针对用户输入的内容进行单独的方向判断。
更进一步,可以考虑使用开源的工具进行方向判断。(笔者正在仿照Flutter的intl工具,提供自动判断文字算法的NPM包,后续有机会会更新到这篇文章里面)
工作流选择国际化支持好的工具
在项目早期,我们使用腾讯文档进行内容翻译工作的组织和进行。
但由于腾讯文档并没有国际化支持,默认全部使用LTR进行文本渲染,当遇到阿拉伯语、英语混排的场景时,会出现文本语序显示错误的情况。
我们的阿拉伯语翻译小姐姐为了解决问题,使用中性字符串代替了英语字符串,从而显示正确的文本顺序。但是这给后期协作带来了很多不必要的麻烦。
深入到布局方向
除了文字的方向不同,在页面布局上,也有RTL和LTR之分;
左和右、开始和结束
先看看Flutter是如何实现RTL布局兼容的:
Flex方向改变
此外,Flutter中元素布局大多使用Flex布局,针对Flex布局方案,RTL模式下主轴方向将从右上角开始,左下角结束。
逻辑方向
在Flutter的世界中,为每个布局属性都提供了等价的Directional属性,Directional属性使用start和end替换了left和right这两种方向描述,我们称之为** 逻辑方向 **。当APP为LTR模式时,start对应left,end对应right;而在RTL模式下则正好相反,start对应right,end对应left;
例如:
// before
Container(
width: 24,
height: 24,
margin: EdgeInsets.only(left: 8),
);
// after
Container(
width: 24,
height: 24,
margin: EdgeInsetsDirectional.only(start: 8),
);
通过这两种方式的兼容,页面便可以在RTL模式下显示正确的布局方向。
在Web的场景下,当设置HTML的dir='rtl',Flex布局的方向就会改变。
CSS也提供了类似于逻辑方向的属性,称之为Logical Properties
blockquote {
text-align: start; /* left in latin, right in arabic */
margin-inline-start: 0px; /* margin-left in latin, margin-right in arabic */
border-inline-start: 5px solid gray; /* border-left in latin, border-right in arabic */
padding-inline-start: 5px; /* padding-left in latin, padding-right in arabic */
}
/* 下面这两种方式等价,由于兼容性问题,请不要用到生产环境 */
blockquote {
margin: logical 1em 2em 3em 4em;
}
blockquote {
margin-block-start: 1em;
margin-inline-start: 2em;
margin-block-end: 3em;
margin-inline-end: 4em;
}
目前Logical Properties规范仍然出于**W3C草案(Draft)**阶段:
出于兼容性考虑,并不推荐在生产环境中使用Logical Properties进行页面布局。
多种方案的权衡
大多也只是权衡利弊罢了...
他山之石:antd如何兼容RTL
antd在4.0版本提供了RTL兼容,详情查看这个PR:
feat: added rtl direction to all of ant-design components
antd通过JavaScript判断页面是否处于RTL环境,并单独书写了RTL样式,对组件一个一个做单独的兼容。
这种改造成本是相当大的,这次PR,作者一共提交了250此Commit,涉及了262个文件的修改,7000多行代码的新增。
那么。是否有其他的方案进行RTL自动适配,减少开发成本,社区提供了一些方案:
rtlcss
RTLCSS通过预处理的方式将CSS中的方向性定位反转(left -> right, right -> left)。
并且能够通过CSS注释的方式,提供更加细粒度的控制。
如下截图所示,分别是原始css和经过RTLCSS处理后的CSS:
RTLCSS提供CLI以及Webpack的方式,可以很方便的集成到现有的工作流中,为项目提供RTL方案的支持。
但由于该方案只能处理CSS,对于写在JS中的内联样式就无能为力了。
总结
Pros
- 完善的工具链支持
- 对开发没有任何侵入性
- 通过侦测用户语言,可以实现RTL样式的按需加载
Cons
- 对内联样式无能为力
Hack的方式:transform: scaleX(-1)
国际化 - 通用 LTR/RTL 布局解决方案 Alibaba-Lindz
Alibaba的Lindz同学想到了一种比较Hack的方式对页面进行镜像翻转:transform: scaleX(-1)
通过 transform: scaleX(-1) 可以使页面沿着中轴进行水平翻转
并且这种方式在布局上具有良好的兼容性,跟 direction 改变方向不同,根本无需考虑你的布局:flex/浮动/绝对定位等等,都可以很好地从 LTR 布局变成 RTL 布局。并且这个方案对于内联样式也可以进行镜像。
但是也引入的新的问题,就是文字,图片等等信息全部都翻转了,所以我们在文字部分需要将文字再翻转回来:
比如说在文字的容器上再次叠加 transform: scaleX(-1),将其再次翻转。
总结
Pros
- 可以实现内联样式的镜像翻转
- 对于第三方库,也可以进行镜像翻转
Cons
- 比较hack,与标准背道而驰
- 多次翻转,可能造成不必要的渲染性能开销
- 对开发侵入大,心智负担较高
事实上,在生产实际中,我们完全可以同时使用两种不同的方案;个人更加推荐使用RTL CSS打包生成两个CSS文件,通过Visibility Change侦测用户语言变化,做到CSS样式的按需加载。
但是,面对一些第三方开源库不支持CSS预处理的情况,则可以使用transform方案进行兼容。
参考文章:
国际化 - 通用 LTR/RTL 布局解决方案 https://github.com/happylindz/blog/issues/16
css3中的unicode-bidi与direction使用 https://juejin.cn/post/6844903688406843406