BIDI
雙向文字就是一個字符串中包含了兩種文字,既包含從左到右的文字又包含從右到左的文字。
大多數文字都是從左到右的書寫習慣,比如拉丁文字(英文字母)和漢字,少數文字是從右到左的書寫方式比如阿拉伯文(ar)跟希伯來文(he)。對於需要國際化支持的web應用來說,由於閱讀習慣的不同在web頁面排版和布局中會給開發人員帶來麻煩。這種情況多數出現在從右到左的文字中,比如字符串中出現阿拉伯文、英文字母、數字以及標點符號。本文就是我在工作中遇到該類問題的總結。
在現代計算機應用中,最常用來處理雙向文字的算法是 Unicode 雙向算法(Unicode Bidirectional Algorithm),在后面的文章中我們將 Unicode 雙向算法簡稱為 bidi 算法。
我們的web產品中使用的字符都屬於unicode字符,而unicode字符的方向屬性總共包含三類:強字符、中性字符、弱字符。
強字符的方向屬性是確定的,與上下文的bidi屬性無關,而且強字符在bidi算法中可能會對其前后的中性字符產生影響。大部分的字符都屬於強字符,比如拉丁字符、漢字、阿拉伯字符。
中性字符的方向性並不確定,受其上下文的bidi屬性影響(前后的強字符)。比如大部分的標點符號(“-”,“[]”,"()"等)跟空格。
弱字符的方向性是確定的,但不會對其上下文的bidi屬性產生影響。比如數字以及跟數字相關的符號。
一個區域內有總體方向,決定從這個區域的哪邊開始書寫文字,通常稱為基礎方向。瀏覽器會根據你的默認語言來設置默認的基礎方向,如英語、漢語的基礎方向為從左到右,阿拉伯語的基礎方向為從右到左。
方向串是指在一段文字中具有相同方向性的連續字符,並且其前后沒有相同方向性的其它方向串。
下方假設大寫字母為從右到左的文字。
<p dir="ltr">The apple is called <bdo dir="rtl">APPLE</bdo> in ar.</p>
在這個例子中,包含了三個方向串。該句子以從左到右的方向串開始,然后是從右到左的方向串,最后以從左到右的方向串結尾。
要注意的是,方向串的排列順序和數目往往會受到全局方向的影響。上面的例子中采用是從左到右的全局方向,如果該全局方向變為從右到左,那這個例句中方向串的排列順序將如下圖所示:
<p dir="rtl">The apple is called <bdo dir="rtl">APPLE</bdo> in ar.</p>
Web中控制文字方向的方式有三種:html實體(‎ ‏)、bid與bdo標簽+dir屬性、css屬性(direction + unicode-bidi)
‎與‏可以用來打斷方向串的連續性,影響中性字符的方向
下面這段文本中共有四個中性字符:"."、 "+"、 "("、")";受從左到右基礎方向影響這幾個字符的方向性都表現為從左到右,界面也是正常的。
<p dir="ltr">My first paragraph.U+202(C)</p>
如果將基礎方向設置為從右到左
<p dir="rtl">My first paragraph.U+202(C)</p>
最右邊的")"受基礎方向影響會出現我們不想要的結果,而其他三個中性字符受上下文方向性影響依舊保持從左到右的方向性。
我們可以使用‎實體來改變")"的方向性。
<p dir="rtl">My first paragraph.U+202(C)‎</p>
在上文介紹方向串時已經看到大寫字母變成從右到左的方向這就是bdo元素+dir的作用,覆蓋元素內文本的方向性。
bdi元素的目的是設置一個隔離區域。如果不設置dir則使用上下文的基礎方向。
<ul> <li>Username Bill:80 points</li> <li>Username <bdi><bdo dir="rtl">Steve</bdo></bdi>: 78 points</li> </ul>
大家可以試試把bdi元素去掉是什么效果,試着分析一下里面的方向串。
如果設置dir屬性那么就為這個隔離區域設置了一個基礎方向。
<p dir="rtl">These fruits <bdi dir="ltr">are called <bdo dir="rtl">APPLE</bdo>, </bdi><bdo dir="rtl">PEAR</bdo> and <bdo dir="rtl">ORANGE</bdo> in Arabic.</p>
注意里面的空格跟標點符號的方向性。
direction跟unicode-bidi這兩個是css屬性,通常放在一起來控制文本的方向,大家可以自己查看一下css手冊。
direction+unicode-bidi:embed 的效果類似於bdi元素; direction+ unicode-bidi: bidi-override 的效果類似於bdo元素。
實際項目中我遇到阿拉伯語下在表格中顯示負數問題,看起來的效果是:“88-”;使用以上屬性direction:ltr,unicode-bidi:embed,可以改變顯示效果:“-88”。
RTL布局
工作中遇到的另一個跟語言相關的問題就是頁面布局問題。阿拉伯文(ar)跟希伯來文(he)的頁面布局同英語下的頁面布局剛好是鏡像關系。這一點大家可以試試把瀏覽器的語言設置為阿拉伯語,觀察一下瀏覽器上的控件布局(要保證你能再設置回來)。
首先判斷用戶設置的語言,如果是ar跟he則將全局基礎方向設置為rtl,這時基本可以解決大多數問題。

function localeIsSame(locale1, locale2){ return locale1.indexOf(locale2) > -1 || locale2.indexOf(locale1) > -1; } function _setRTL(locale){ var rtlLocales = ["ar", "he"]; var dirNode = document.getElementsByTagName("html")[0]; var isRTLLocale = false; for (var i = 0; i < rtlLocales.length; i++) { if (localeIsSame(rtlLocales[i], locale)) { isRTLLocale = true; } } if (isRTLLocale) { dirNode.setAttribute("dir", "rtl"); dirNode.className += " esriRtl jimu-rtl"; dirNode.className += " " + locale + " " + (locale.indexOf("-") !== -1 ? locale.split("-")[0] : ""); }else { dirNode.setAttribute("dir", "ltr"); dirNode.className += " esriLtr jimu-ltr"; dirNode.className += " " + locale + " " + (locale.indexOf("-") !== -1 ? locale.split("-")[0] : ""); } window.isRTL = isRTLLocale; }
然后將float和text-align以及控制間距的margin、padding從所有的css class中抽離出來單獨成類,如:
在RTL時將他們左右互換
在代碼中與rtl布局相關的部分通過判斷全局變量window.isRTL來處理。

html[dir='rtl'] caption,
html[dir='rtl'] th {
text-align: right;
}
.jimu-rtl {
direction: rtl;
}
.jimu-align-trailing {
text-align: right;
}
.jimu-align-leading {
text-align: left;
}
.jimu-float-trailing {
float: right;
}
.jimu-float-leading {
float: left;
}
.jimu-numeric-value {
direction: ltr;
unicode-bidi: embed;
}
/* if a ltr element is inside a rtl page */
.jimu-ltr .jimu-float-leading {
float: left !important;
}
/* RTL alignment */
.jimu-rtl .jimu-align-trailing {
text-align: left;
}
.jimu-rtl .jimu-align-leading {
text-align: right;
}
.jimu-rtl .jimu-float-trailing {
float: left;
}
.jimu-rtl .jimu-float-leading {
float: right;
}
/******* margins ******/
.jimu-leading-margin0 {
margin-left: 0;
}
.jimu-leading-margin025 {
margin-left: 0.25em;
}
.jimu-leading-margin05 {
margin-left: 0.5em;
}
.jimu-leading-margin1 {
margin-left: 1em;
}
.jimu-leading-margin15 {
margin-left: 1.5em;
}
.jimu-leading-margin2 {
margin-left: 2em;
}
.jimu-leading-margin25 {
margin-left: 2.5em;
}
.jimu-leading-margin3 {
margin-left: 3em;
}
.jimu-leading-margin35 {
margin-left: 3.5em;
}
.jimu-leading-margin4 {
margin-left: 4em;
}
.jimu-leading-margin5 {
margin-left: 5em;
}
.jimu-leading-margin6 {
margin-left: 6em;
}
.jimu-leading-margin7 {
margin-left: 7em;
}
.jimu-leading-margin10 {
margin-left: 10em;
}
.jimu-trailing-margin025 {
margin-right: 0.25em;
}
.jimu-trailing-margin05 {
margin-right: 0.5em;
}
.jimu-trailing-margin075 {
margin-right: 0.75em;
}
.jimu-trailing-margin1 {
margin-right: 1em;
}
.jimu-trailing-margin15 {
margin-right: 1.5em;
}
.jimu-trailing-margin2 {
margin-right: 2em;
}
.jimu-trailing-margin25 {
margin-right: 2.5em;
}
.jimu-trailing-margin3 {
margin-right: 3em;
}
.jimu-trailing-margin35 {
margin-right: 3.5em;
}
.jimu-trailing-margin4 {
margin-right: 4em;
}
.jimu-trailing-margin5 {
margin-right: 5em;
}
.jimu-trailing-margin6 {
margin-right: 6em;
}
.jimu-leading-padding05 {
padding-left: 0.5em;
}
.jimu-leading-padding1 {
padding-left: 1em;
}
.jimu-trailing-padding1 {
padding-right: 1em;
}
/* RTL related: margins, padding */
.jimu-rtl .jimu-leading-margin0 {
margin-right: 0;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin025 {
margin-right: 0.25em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin05 {
margin-right: 0.5em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin075 {
margin-right: 0.75em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin1 {
margin-right: 1em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin15 {
margin-right: 1.5em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin2 {
margin-right: 2em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin25 {
margin-right: 2.5em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin3 {
margin-right: 3em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin35 {
margin-right: 3.5em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin4 {
margin-right: 4em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin5 {
margin-right: 5em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin6 {
margin-right: 6em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin7 {
margin-right: 7em;
margin-left: auto;
}
.jimu-rtl .jimu-leading-margin10 {
margin-right: 10em;
margin-left: auto;
}
.jimu-rtl .jimu-trailing-margin025 {
margin-left: 0.25em;
margin-right: auto;
}
.jimu-rtl .jimu-trailing-margin05 {
margin-left: 0.5em;
margin-right: auto;
}
.jimu-rtl .jimu-trailing-margin075 {
margin-left: 0.75em;
margin-right: auto;
}
.jimu-rtl .jimu-trailing-margin1 {
margin-left: 1em;
margin-right: auto;
}
.jimu-rtl .jimu-trailing-margin15 {
margin-left: 1.5em;
margin-right: auto;
}
.jimu-rtl .jimu-trailing-margin2 {
margin-left: 2em;
margin-right: auto;
}
.jimu-rtl .jimu-trailing-margin25 {
margin-left: 2.5em;
margin-right: auto;
}
.jimu-rtl .jimu-trailing-margin3 {
margin-left: 3em;
margin-right: auto;
}
.jimu-rtl .jimu-trailing-margin4 {
margin-left: 4em;
margin-right: auto;
}
.jimu-rtl .jimu-trailing-margin5 {
margin-left: 5em;
margin-right: auto;
}
.jimu-rtl .jimu-trailing-margin6 {
margin-left: 6em;
margin-right: auto;
}
.jimu-rtl .jimu-leading-padding05 {
padding-right: 0.5em;
padding-left: auto;
}
.jimu-rtl .jimu-leading-padding1 {
padding-right: 1em;
padding-left: auto;
}
.jimu-rtl .jimu-trailing-padding1 {
padding-left: 1em;
padding-right: auto;
}
參考文章: