在业务全球化的进程中,我们会面对产品本地化的需求。在中东地区,许多国家使用阿拉伯语、希伯来语等语言,其书写和阅读习惯是从右向左(简称 RTL),与我们日常使用的中、英文环境中的从左向右(简称 LTR)阅读习惯相反。为了确保我们的产品在 RTL 语言用户中依然能够提供良好的体验,需要进行 RTL 适配。
如上图所示,左右两边分别展示了 RTL 和 LTR 的效果图。从图中我们可以直观地看出两者布局的区别:文本的对齐方向、主按钮和辅助按钮的排列方向、进度条的填充方向以及返回图标的方向是相反的,而其他图标则是相同的。具体总结如下:
LTRRTL文本句子从左向右阅读句子从右向左阅读时间线事件序列从左向右进行事件序列从右向左进行图像从左向右的箭头表示向前运动:→从右向左的箭头表示向前运动:←了解了 RTL 布局的特点之后,我们可以开始考虑如何低成本地将线上已有场景的 UI 从 LTR 调整为 RTL。在将 UI 从 LTR 调整为 RTL(或反之)时,我们通常称之为镜像。
基于 transform 的方案,是利用 CSS 的 transform 属性,通过设置 transform: scaleX(-1);
实现页面的水平翻转。
如上图所示,通过翻转解决了布局问题,但文字和图像也被翻转。为了解决这个问题,对于不需要翻转的内容(如文字、非指向性图像),需要进行二次翻转。然而,该方案的缺点在于,首次翻转只需要处理根节点,而二次翻转则需要处理所有不需要翻转的元素,工作量较大。该方案的优点在于开发者无需修改 JS 逻辑。例如,通常情况下,左滑/左向箭头图标的点击事件在 RTL 时会将前进改为后退,右向将后退改为前进。
direction
基于 direction 的方案,是利用 CSS 的 direction 属性,该属性用于设置文本、表格列和水平溢出的方向。通过将 direction 设置为 rtl 可以改变页面布局,在 html 标签上添加 dir="rtl"
与设置 direction 效果相同。我们通过一个简单的例子来具体了解设置为 rtl
的效果。
ltr-rtl.png如上图所示,设置为 rtl
之后,我们发现 UI 并没有完全兼容 RTL 场景。我们可以观察到,direction 在设置 rtl
之后只对部分属性进行了镜像处理:
text-align
,那么该元素的文本会从向左对齐变成向右对齐,如果设置了 left/center
则 direction 的设置不会对其产生影响inline-block
、flex
、table
、grid
的布局方向被影响,absolute
/ fixed
、float
、margin
、padding
无任何变化。为了页面能够在 RTL 布局时正常呈现,我们需要对未被影响的属性调整。目前有以下两种方式可以解决这个问题。
CSS 逻辑属性与逻辑值
逻辑属性和逻辑值用抽象术语块向和行向描述其流向。块向尺度(block
)是指与行内文本流向垂直的方向上的尺度。行向尺度(inline
)是指与行内文本流向平行的方向上的尺度。LTR 布局时,block-start
对应 top
,block-end
对应 bottom
, inline-start
对应 left
,inline-end
对应 right
,inline-size
对应 width
,block-size
对应 height
。
通过改写为逻辑属性,可以同时适配 LTR 和 RTL 布局,无需专门为 RTL 布局进行适配。例如,将 margin-left
改写成 margin-inline-start
,将 left: 0;
改写成 inline-start: 0;
。我们只需要全局替换需要调整的行向尺度的 CSS 属性即可。然而,使用逻辑属性存在两个问题:一方面是浏览器的兼容性问题(B 端项目可以考虑使用,浏览器兼容性较好),另一方面开发者只能处理本地代码,无法处理 npm 包中的代码。
CSS 翻转工具
另一个方案是使用 CSS 转换工具(rtlcss、css-flip),按照 RTL 布局对 CSS 代码进行转换,例如将 margin-left
改写成 margin-right
,将 left: 0;
改写成 right: 0;
。我们可以在代码构建过程中使用这类工具,自动将 CSS 代码转换为对应的 RTL 布局代码,这样开发者仍然可以按照 LTR 的布局书写代码。
与 CSS 逻辑属性相比,使用 CSS 转换工具是更好的选择。通过这种方案,可以完美解决布局镜像的问题。然而,direction 还存在另一个缺点,即它仅适用于 CSS,涉及 JS 就无能为力。
我们希望以较低的成本改造线上已有的 UI 场景,以支持 RTL 布局。大部分业务内容中的文本和图片无需翻转,因此使用 transform 方案逐一适配这部分内容会带来大量工作量,需要编写大量影响业务逻辑的代码。在业务迭代过程中,开发人员需要不断处理二次翻转的问题。相比之下,使用 direction 方案能减少开发者对哪些模块需要翻转的关注,只需对个别组件的 JS 逻辑进行适配。在权衡利弊后,我们选择了基于 direction 的方案。接下来,我们对该方案进行细化和完善。
基于 direction 通用适配方案
首先,我们要基于用户语言,在 html 标签设置属性 dir。语言的获取可以从 URL
的 search
属性或 cookie
。我们提供一个工具库进行初始化设置,同时提供了更新方法 setDirecion
、根据语言判断是否需要 RTL 布局的工具函数 isRTL
。
import { Cookie } from '@music/helper';
import { parse } from '@music/mobile-url';
const rtlLngs = ['ar-EG', 'he_IL'];
export default class RTL {
private lng: string;
constructor(lng?: string) {
this.lng = lng || '';
if (typeof window !== 'undefined') {
const { location } = (window as Window);
if (!this.lng) {
this.lng = (parse(location.search) as any).language || Cookie.get('language') || 'en-US';
}
document.documentElement.setAttribute('dir', rtlLngs.includes(this.lng) ? 'rtl' : 'ltr');
}
}
setDirecion(lng?: string) {
this.lng = lng || '';
document.documentElement.setAttribute('dir', rtlLngs.includes(this.lng) ? 'rtl' : 'ltr');
}
static isRTL(lng?: string) {
if (lng) return rtlLngs.includes(lng);
if (typeof window !== 'undefined') {
const { location } = (window as Window);
const l = (parse(location.search) as any).language || Cookie.get('language') || 'en-US';
return rtlLngs.includes(l);
}
return false;
}
}
使用时非常简单,在页面入口文件引入该模块即可。
import RTL from '@music/tl-rtl';
new RTL();
SSR 无法从 document
/ window
获取 cookie
/ URL
的 search
属性,所以需要通过 getInitialData
获取存储在 store 中,然后通过 Helmet 设置 html 的 dir 属性。
import { createUrl, parse } from '@music/mobile-url';
// 获取 isRTL 并存储 store
static getInitialData({ req }) {
const { url, header } = req;
const cookieLng = headers?.cookie
?.split(';')
.map((c) => c?.split('='))
?.find((c) => c[0]?.trim() === 'language')?.[1];
const lng = parse(createUrl(url).search).language || cookieLng || 'en-US';
const isRTL = RTL.isRTL(lng);
... // 选择合适的 store 方案存储 isRTL 值
}
import { Helmet, HelmetProvider } from 'react-helmet-async';
// 从 store 获取 isRTL 并设置 html dir
function App ({ isRTL }) {
return (
<HelmetProvider>
<div>
<Helmet>
<html dir={isRTL ? 'rtl' : 'ltr'} />
</Helmet>
...
</div>
</HelmetProvider>
);
}
接下来就需要转换 CSS 代码适配 RTL。前面我们说到了选用 CSS 转换工具处理 CSS 代码这一步最好在构建过程中完成,postcss-rtlcss(基于 rtlcss)很好的满足了这一特点,它作为 PostCSS 插件可以在 webpack 构建过程中可以将所有本地代码和 npm 包中的 CSS 文件统一处理。
下面是 postcss-rtlcss 的使用方式,及一些关键参数的解析。
import { postcssRTLCSS } from 'postcss-rtlcss';
import { Mode } from 'postcss-rtlcss/options';
const defaultOptions = {
mode: Mode.combined,
ignorePrefixedRules: true,
ltrPrefix: '[dir="ltr"]',
rtlPrefix: '[dir="rtl"]',
bothPrefix: '[dir]',
};
const options = {
...defaultOptions,
safeBothPrefix: true,
processUrls: true,
processKeyFrames: true,
useCalc: true,
};
export default {
module: {
rules: [
{
test: /\.css$/,
use: [
...
{ loader: 'css-loader' },
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
postcssRTLCSS(options)
]
}
}
}
...
]
},
]
}
}
mode
该参数控制了 CSS 的生成方式,三种模式分别输出的 CSS 代码如下所示。
/* input */
.test1 {
width: 10px;
padding: 10px;
}
.test2 {
padding-right: 20px;
}
/* output Mode.diff */
.test1 {
width: 10px;
padding: 10px;
}
.test2 {
padding-left: 20px;
padding-right: 0;
}
/* output Mode.override */
.test1 {
width: 10px;
padding: 10px;
}
.test2 {
padding-right: 20px;
}
[dir="rtl"] .test2 {
padding-left: 20px;
padding-right: 0;
}
/* output Mode.combined */
.test1 {
width: 10px;
padding: 10px;
}
[dir="ltr"] .test2 {
padding-right: 20px;
}
[dir="rtl"] .test2 {
padding-left: 20px;
}
我们的需求是用一份代码根据语言同时适配 LTR 和 RTL 布局。Mode.diff
模式会将 CSS 代码转换为 RTL 布局的代码,无法同时适配两种布局,因此首先排除。另外两种模式 Mode.override
、Mode.combined
则可以生成两种布局的代码。然而,Mode.override
模式在样式覆盖的情况下转换处理会出现一些问题。如上所示,在 RTL 布局时 padding-right
最终生效值是 0
,与期望的 10px
不符。为了符合预期,我们需要给 .test2
增加一行代码 padding-left: 10px;
。而 Mode.combined
模式无需额外处理现有代码即可生成符合预期的代码。
因此,我们最终选择 Mode.combined
模式,该模式会将需要处理的 CSS 代码生成两份,以便在渲染时对应生效。接下来的 demo 输出的 CSS 都是基于此模式。
safeBothPrefix
该参数设置为 true
时 CSS 输出结果如下所示,即会给不需要翻转的方向性 CSS 属性类名增加 bothPrefix
([dir]
)。在 class="test1 test2"
时,可以按照 CSS 书写顺序使得 .test2
的 padding
样式能正确覆盖 .test1
的。设置为 false
时输出的 .test2
的规则名保持不变,不会变成 [dir] .test2
,按照 CSS 选择器权重会导致最终生效的是 [dir="ltr"].test1
/ [dir="rtl"].test1
对应的 padding
样式,与期望不符。
/* input */
.test1 {
padding: 0 10px 0 20px;
}
.test2 {
padding: 0 20px;
}
/* output */
[dir="ltr"] .test1 {
padding: 0 10px 0 20px;
}
[dir="rtl"] .test1 {
padding: 0 20px 0 10px;
}
[dir] .test2 {
padding: 0 20px;
}
processUrls
该参数控制是否按照字符串映射来翻转更改 URL 中的字符串,例如 ltr``left
。当设置为 false
不会处理 URL 地址,当设置为 true
会翻转处理如下所示。
/* input */
.test {
background-image: url("./img/ltr/arrow-left.png");
}
/* output */
[dir="ltr"] .test {
background-image: url("./img/ltr/arrow-left.png");
}
[dir="rtl"] .test {
background-image: url("./img/rtl/arrow-right.png");
}
ignorePrefixedRules
该参数值为 true
会忽略 CSS 选择器中包含 rtlPrefix
、ltrPrefix
、bothPrefix
的 CSS 规则,不进行转换。当设置为 false
会被转换为如下所示,导致 CSS 选择器无法匹配,从而使样式失效。
/* input */
[dir="rtl"] .test {
left: 10px;
}
/* output */
[dir="ltr"] [dir="rtl"] .test {
left: 10px;
}
[dir="rtl"] [dir="rtl"] .test {
right: 10px;
}
前文我们说到,指向性图像需要在 RTL 布局时翻转,而 ignorePrefixedRules 和 processUrls 恰好可以用来处理这种情况。processUrls 适用于本地资源,本地存放 2 份资源图片即可;ignorePrefixedRules 可同时作用于远程资源,增加下面的全局样式(该样式不会被转换,且仅在 RTL 布局生效),并给需要翻转的图片增加 flip-img
类名即可。
[dir="rtl"] .filp-img {
transform: scaleX(-1);
}
useCalc
该参数控制是否翻转 background-position-x
和 transform-origin
,当设置为 false 时不处理,当设置 true
会被转换为如下所示。
/* input */
.test {
background-position-x: 5px;
transform-origin: 10px 20px;
}
/* output */
[dir="ltr"] .test {
background-position-x: 5px;
transform-origin: 10px 20px;
}
[dir="rtl"] .test {
background-position-x: calc(100% - 5px);
transform-origin: calc(100% - 10px) 20px;
}
processKeyFrames
该参数控制是否翻转关键帧动画中的样式规则,考虑到动画中也会存在左右移动的情况,设置为 true
。
更多参数设置可以查看 options了解。
由于 postcss-rtlcss 插件只处理样式文件,所以 CSS 都要书写在样式文件中,如非必要,不要使用如下内联样式,
<div style={{ marginLeft: 10 }}>
...
</div>
如果必须使用内联样式,比如说需要在 JS 中计算 CSS 属性值,需要业务自行适配 RTL 布局。
第三方库的适配
在业务开发时我们通常会用到一些三方组件,例如 antd
、Swiper
,我们需要考虑这些组件如何适配 RTL。
antd
antd
已经支持了 RTL 布局,需要进行如下配置即可(本文讨论的 antd
基于 4.x 版本)。
import { ConfigProvider } from 'antd';
export default ({ isRTL }) => (
<ConfigProvider direction={isRTL ? 'rtl' : 'ltr'}>
<App />
</ConfigProvider>
);
配置之后我们发现展示结果与期望不符,排查发现是因为 antd
已经根据 direction 对组件的类名和 CSS 样式做了镜像处理。
// ltr
<Component className="ant-xxx" />
// rtl
<Component className="ant-xxx ant-xxx-rtl" />
.ant-xxx {
margin: 0 8px 0 0;
}
.ant-xxx.ant-xxx-rtl {
margin-left: 8px;
margin-right: 0;
}
在配置 postcss-rtlcss
插件之后,CSS 代码会被处理成下面的代码,导致在 RTL 布局时,根据书写顺序和 CSS 选择器优先级最终按照 [dir="rtl"].ant-xxx.ant-xxx-rtl
渲染,导致结果错误。
/* output */
[dir="ltr"] .ant-xxx {
margin: 0 8px 0 0;
}
[dir="rtl"] .ant-xxx {
margin: 0 0 0 8px;
}
[dir="ltr"] .ant-xxx.ant-xxx-rtl {
margin-left: 8px;
margin-right: 0;
}
[dir="rtl"] .ant-xxx.ant-xxx-rtl {
margin-right: 8px;
margin-left: 0;
}
所以,在配置 postcss-rtlcss
插件时需要将 antd
的样式资源 exclude
,保证其 CSS 资源不被镜像处理。
Swiper
Swiper
组件也适配了 RTL 布局,只需要在其祖先节点设置 dir="rtl"
即可,而我们的方案就是在 html 标签设置 dir,无需要额外处理。
其他涉及 JS 层面需要适配 RTL 的私有组件需要开发者获取 dir 的值,并对组件进行适配改造。
在开发调试过程中,我们提供了一个语种快速切换工具,便于预览对应的 LTR 和 RTL 的布局效果。
该工具的具体实现如下:
import React, { useCallback } from 'react';
import reactDOM from 'react-dom';
import Select from 'antd/lib/select';
import { parse, stringify } from '@music/mobile-url';
import { Cookie } from '@music/helper';
const rtlLngs = ['ar-EG', 'he_IL'];
const i18nMap = {
'zh-CN': '简体中文',
'en-US': '英文',
'ar-EG': '阿拉伯语',
};
// 创建语种切换组件
const SwitchLng = ({ lngs }) => {
const lng = parse(window.location.search).language || Cookie.get('language') || 'en-US';
const handleSwitch = useCallback((l) => {
// cookie 更新语种
Cookie.set('language', l);
// 替换 url 语种参数并 reload 页面
const searchStrs = parse(window.location.search) || {};
searchStrs.language = l;
const { origin, pathname } = window.location;
window.location.href = `${origin}${pathname}?${stringify(searchStrs)}`;
}, []);
return (
<Select
style={{ position: 'fixed', bottom: 10, left: 10, width: 140 }}
defaultValue={lng}
onChange={handleSwitch}>
{lngs.map((l) => (
<Select.Option value={l}>
{i18nMap[l]}
{rtlLngs.includes(l) && (
<span style={{ color: 'red', marginLeft: 5 }}>RTL</span>
)}
</Select.Option>
))}
</Select>
);
};
class RTLHelper {
constructor(lngs) {
const l = (lngs || []).map((e) => e.replace('_', '-'));
const allLngs = ['en-US', 'ar-EG', 'zh-CN'].concat(l);
this.supportLngs = [...new Set(allLngs)];
this.renderDOM();
}
// 渲染组件到页面中
renderDOM() {
const btn = document.createElement('div');
document.body.appendChild(btn);
reactDOM.render(<SwitchLng lngs={this.supportLngs} />, btn);
}
}
export default RTLHelper;
使用时在 dev 文件中引用即可。
import RTLHelper from '@music/tl-rtl/helper';
new RTLHelper();
本文介绍了云音乐出海业务中 Web 项目对 RTL 语言的适配实践,并总结为一套通用高效的方案。该方案使开发者在处理业务需求时无需过多关注样式适配问题,为开发者提供了便捷高效的开发体验。
本文由微信公众号奇舞精选原创,哈喽比特收录。
文章来源:https://mp.weixin.qq.com/s/xZBQkvOI884MwWga0cjtFA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。