为了让德语长词在手机上不撑破,前端往标题里塞了个看不见的字符

为了让德语长词在手机上不撑破,前端往标题里塞了个看不见的字符
张文保 更新 36 分钟阅读 2,371 阅读
本文目录
  1. 为什么德语页面在手机上会横向溢出?
  2. 复合词的长度分布跟英语完全不是一回事
  3. 容器宽度是按英语文案标定的
  4. 溢出最先出现在哪几类组件上
  5. 一个数字:最长的那个词有多少字符
  6. 前端最常用的三种修补,分别落在哪一层?
  7. 字符层、标记层、样式层
  8. 判别法:写在字符串里还是写在样式表里
  9. 三种修补的可逆性完全不同
  10. 软连字符进了标题之后会发生什么?
  11. 标题去重脚本会把它当成一个新词
  12. 复制粘贴出来的关键词对不上
  13. 结构化数据的name字段被一起污染
  14. 搜索引擎读到的到底是几个词
  15. 零宽空格为什么在东南亚语言的站上到处都是?
  16. 泰语的断行边界必须靠人插进去
  17. 粘贴动作是这类字符的主要来源
  18. 泰语那半边的细节交给分词那一篇
  19. 这些字符怎么检测出来?
  20. 一条正则就能扫全站
  21. 数据库层怎么查
  22. 导出关键词表时的清洗顺序
  23. 为什么不该在存储层清洗
  24. CSS层的解法能覆盖到哪一步?
  25. overflow-wrap管的是什么
  26. word-break为什么更危险
  27. 容器宽度才是根因
  28. 还有一档解决的是别的问题
  29. 连字断词功能为什么在你的站上不生效?
  30. lang属性没写对是最常见的原因
  31. 浏览器的连字词典覆盖有限
  32. 字体跟断词没有关系
  33. 怎么验证它到底生效没有
  34. 哪些位置绝对不能出现不可见字符
  35. 标题、地址和结构化数据三处零容忍
  36. 关键词表和日志也要保持干净
  37. 可以放的地方只有正文里的长词
  38. 一套断行方案的选型顺序
  39. 四步选型
  40. 一张能贴在评审清单上的表
  41. 换行规则本身在不同语言里差在哪?
  42. 有空格的语言和没空格的语言
  43. 同一套算法在不同语言上的落点
  44. 常见问题解答
  45. 我们站上没有德语,还需要管这件事吗?
  46. 怎么在十分钟内判断自己站上有没有这个问题?
  47. 已经插进数据库的那些字符,要不要全部清掉?
  48. 软连字符会不会直接影响排名?
  49. 自动断词打开之后,需要担心断点断错吗?
  50. 这件事该归前端还是归SEO?
  51. 这跟去掉变音符号、大小写转换那些坑是同一类吗?
  52. 权威参考资料

摘要:德语的长复合词在手机上会把布局撑破,最省事的修法是往词中间插一个软连字符;泰语没有空格,最省事的修法是插零宽空格。这两个字符肉眼看不见,却实实在在地写进了HTML,于是标题去重脚本、关键词表、结构化数据和搜索引擎读到的都是被切开的两个词。本文把排版层的三种修补和它们各自的污染范围拆开,给出一条十分钟能扫全站的检测正则。

为什么德语页面在手机上会横向溢出?

复合词的长度分布跟英语完全不是一回事

德语的复合词是把几个名词直接拼在一起,中间不加空格。

常用商品词里二十个字母以上的很普遍,三十个字母也不罕见。

英语表达同样的概念会拆成三四个词,中间有空格。

空格是断行机会,有空格的文本可以在任何一处折行。

没有空格的一长串字符,浏览器默认不会从中间切开。

这是排版引擎的正确行为,不是缺陷。默认的断行规则只允许在词与词之间断开,而复合词在语法上就是一个词,引擎没有理由也没有依据知道该在哪个音节之间切。于是这一整串字符会被当成一个不可分割的盒子,宽度超过容器就直接顶出去。

还有一个容易被忽略的因素是德语的复合能力是开放的,新的组合随时可以造出来而不需要进词典。也就是说你没法通过统计现有词表的长度分布来给宽度封顶,因为下一个上架的商品可能带来一个更长的词,容器必须按弹性来设计而不是按一个固定的最大值。

容器宽度是按英语文案标定的

大多数模板的容器宽度是在英语版本上试出来的。

试的时候用的是英语商品名,最长的那个也就十来个字母。

换成德语内容之后,同一个容器要装两倍长的字符串。

芬兰语、匈牙利语、荷兰语都有同样的问题,程度不同而已。

问题最先出现在窄的那几个位置:卡片、按钮、面包屑、筛选项。

这一层的根因不在断行也不在字体,在于宽度预算这件事从来没有按目标语言重新算过。检查办法很直接:把每种语言的商品名按字符数排个序,取最长的那几个,逐个塞进最窄的那个组件里看会不会顶出去,一小时能扫完全站的关键组件。

这件事在设计评审阶段几乎不可能被发现,因为评审时用的是设计稿里的示例文案,而示例文案默认是英语的。要让它显形,最省事的做法是让设计师在稿子里放一条真实的德语商品名,只要放一条,问题当场就会暴露出来。

溢出最先出现在哪几类组件上

商品卡片的标题区是第一个,因为它宽度固定且通常只给两行。

筛选面板是第二个,那一栏的宽度往往只有一百多像素。

面包屑是第三个,它是单行的,一旦超宽就会把整页撑出横向滚动。

表格的表头是第四个,列宽被内容撑开之后整张表都会变形。

按钮上的文字是第五个,它超出之后会直接盖住旁边的元素。

这五个位置有一个共同点,就是它们的文案通常来自数据库字段而不是手写的页面文案。手写文案的作者能看到效果,会自己调整措辞;数据库字段是批量灌进去的,没有人逐条看过它在页面上长什么样。阿拉伯语在窄屏上的类似情况另见阿拉伯语网站搬进手机之后要重验的那份清单

还有一类位置比这五个更隐蔽,就是那些默认隐藏、只在特定状态下才展开的组件,比如下拉菜单、悬浮提示、移动端的折叠筛选。它们不在常规的页面截图里,测试时也很少被逐个展开,往往要等用户反馈才会被发现。

一个数字:最长的那个词有多少字符

做这件事之前先取一个数,就是你的商品名字段里最长的那一条。

这个数字一拿出来,很多讨论就不需要发生了。

英语站上这个数字通常在三十以内,德语站上经常超过五十。

拿这个数字去乘以字号,就能算出它需要多宽的容器。

算出来的宽度如果超过手机屏幕,那这条内容必然要断行。

顺便建议把这个数字加进上线前的检查项,每次新增语言或者新增品类都重新取一次。它是一个成本几乎为零的先行指标,比等到用户反馈横向滚动条要早得多。字体本身带来的开销另见网页字体在小语种站上占掉的首屏成本

取这个数的时候顺便看一眼它的分布,别只看最大值。如果只有极个别几条特别长,那可能是数据录入问题而不是语言问题,改那几条比改整套模板划算得多;如果长条目占了一两成,那就是实打实的语言层特征,必须在版式上解决。

前端最常用的三种修补,分别落在哪一层?

字符层、标记层、样式层

第一种修补是往词里插一个软连字符,也就是往字符串里加字符。

第二种是插一个换行机会标签,加的是HTML标记不是字符。

第三种是给容器加断行相关的样式规则,不动内容一个字。

三种办法在页面上的视觉效果可以做到几乎一样。

但它们进入内容的深度完全不同,这个差别决定了后果。

第一种的字符会成为文本节点的一部分,凡是读取文本的程序都会读到它;第二种是元素,多数取文本的接口会把它跳过,但不是全部;第三种完全不进入内容,只影响渲染。三者的污染范围从大到小排成一条清晰的梯度。

这三层的顺序不是随便排的,它同时也是撤销成本从高到低的顺序。字符层最难撤,因为它散落在数据里;标记层次之,因为它集中在模板里;样式层最容易,因为它只在一两个文件里。选型时先想撤销成本,往往能避开后面最贵的那笔账。

判别法:写在字符串里还是写在样式表里

面对任何一个排版修补,问一句它最终写在哪里就够了。

写进样式表的,内容不受影响,改起来也不影响别的东西。

写进字符串的,它就是内容的一部分,会被一起存、一起传、一起索引。

这条判别法不需要懂排版,也不需要懂那门语言。

它只需要你去看一眼这个修补最后落在了哪个文件里。

这条判别法能推广到很多地方:为了对齐而在文案里打的空格、为了显示好看而在数字里加的分隔符、为了防止折行而插的不换行空格,全都属于写进字符串的那一类。凡是为了让眼睛舒服而改动了字符串本身的动作,机器都会一起读到

这条判别法还有一个变体,就是问它会不会跟着复制粘贴一起走。会跟着走的说明它在文本节点里,不会跟着走的说明它在样式或者标记里。这个测试连开发工具都不用开,选中文字复制到记事本里看一眼就知道。

三种修补的可逆性完全不同

样式层的修补随时可以撤,改一行CSS就回到原样。

标记层的修补要改模板,范围可控但需要发一次版。

字符层的修补一旦进了数据库,撤销就变成一次数据清洗。

而数据清洗的麻烦在于你得先找出所有被改过的记录。

那些字符肉眼看不见,肉眼核对这条路是走不通的。

更麻烦的是这类字符往往不只在一个地方。商品名改了,同一份内容还被复制到了标题标签、结构化数据、导出的商品数据源、发给平台的数据文件里,清洗的时候每一处都要单独处理,漏一处就会留下一个长期不一致的字段。

还有一种情况比数据清洗更麻烦,就是那些字符已经进了外部系统。你把商品数据源推给了平台、推给了比价站、推给了合作方,那边的库里也存了一份,而你没有权限去清。这时候唯一能做的是修正上游之后重推一次,然后等对方更新。

软连字符进了标题之后会发生什么?

标题去重脚本会把它当成一个新词

大部分站都有一套检查标题重复的脚本,按字符串比对。

插了软连字符的标题跟没插的那条,字符串不相等。

于是脚本认为这是两条不同的标题,不报重复。

反过来,你想找某个词的所有页面时,搜出来的结果会少一批。

少的那一批正好是被排版处理过的那些,通常是最重要的几个页面。

这类问题的隐蔽性在于两个方向的错都不会报错:该报重复的没报,该搜到的没搜到,两个结果看起来都像是正常的。要发现它,只能主动去做一次不可见字符的扫描,没有任何现成的告警会替你发现。

这类问题的排查还有一个特征:它通常是在做别的事情时顺带发现的。有人在核对某个数字对不上,一路查下去才发现字符串里多了个东西。所以主动扫一遍的价值不只是修掉现有问题,更是省掉将来某次排查中被它绊住的那几个小时。

顺带一提,同一类比对失败在词形层面也会发生,只是原因完全不同:那边是同一个词的两种形态对不上,这边是同一个形态多了个看不见的字符。用户实际在打的那个形态另见用户搜的那个短词你的词典里查不到

复制粘贴出来的关键词对不上

做关键词核对的时候,习惯是从页面上把词复制下来贴进表格。

复制的时候软连字符会跟着一起被复制,你看不见它。

贴进表格之后跟工具导出的词一比对,显示不匹配。

你会以为是自己抄错了,重新抄一遍,还是不匹配。

这个循环能耗掉半小时,而问题从头到尾都不在你这一边。

可以直接查Unicode官方关于不显示字符的说明确认这一族字符的行为定义,它们被设计成在多数情况下不产生可见字形,而这个设计目的正是它们难以被人工发现的原因。

顺手提一个能省事的习惯:核对关键词的时候别用眼睛比对,用一个能显示字符数的地方粘一下。两个看起来一样的字符串如果字符数不同,问题立刻定位到了字符层,不需要再去怀疑是不是自己抄错了。

结构化数据的name字段被一起污染

商品的结构化数据通常直接取商品名字段的值。

字段里有什么就输出什么,包括那些看不见的字符。

于是搜索引擎读到的商品名跟你以为的不是同一个字符串。

这会影响实体匹配,也会让平台侧的数据校验偶尔报错。

报错的时候提示往往很模糊,只说值不合法,不说哪里不合法。

更稳妥的做法是在输出结构化数据之前做一次显式的清洗,把这一族字符全部剥掉。清洗必须发生在输出层而不是存储层,因为页面渲染那一侧还需要它们。结构化数据字段的填法另见小语种结构化数据的语言与地区字段怎么填

结构化数据这一层还有一个特殊之处,就是它的错误反馈是延迟且模糊的。页面渲染错了你马上能看见,结构化数据里带了脏值可能几个月都没有任何提示,直到某天平台侧的校验规则收紧才集中爆发出来。

搜索引擎读到的到底是几个词

软连字符的定义是一个只在断行处显示的连字符。

不在断行处的时候,它既不显示也不应该影响词的完整性。

按规范理解,它不该把一个词切成两个。

但这依赖于处理它的每一个环节都正确实现了规范。

你的CMS、你的搜索引擎、你的数据导出脚本,未必每个都实现了。

断行算法本身的定义可以查Unicode标准附件14中关于软连字符的一节,那里说明了它的断行类别和预期行为。规范写得很清楚,但规范约束不了那些自己写了一个正则去切词的中间环节,而真实链路上这样的环节比你以为的多。

实际上要不要担心这一条,取决于你的链路上有多少个自己写的中间环节。全套用成熟框架的站风险低,中间夹了自研的导出脚本、清洗脚本、数据源生成器的站风险高,因为每一段自研代码都是一次重新实现规范的机会。

零宽空格为什么在东南亚语言的站上到处都是?

泰语的断行边界必须靠人插进去

泰语句子里没有空格,词与词之间没有任何可见标记。

浏览器要断行就需要知道词边界,而词边界要靠词典来判断。

浏览器对泰语的词典支持这些年好了很多,但不是所有环境都有。

在支持不好的环境里,长句子会整条溢出容器。

最直接的修法就是在词与词之间插一个零宽空格。

这个字符宽度为零,肉眼完全看不出来,但它给了浏览器一个断行机会。作为排版手段它非常有效,问题跟软连字符一样,它进了内容。泰语的分词问题整体另见泰语标题里找不到一个空格,切在哪儿由引擎的词典说了算

要注意浏览器对泰语断行的支持是这些年逐步补上的,早期那批插进去的字符很多是当年确实需要的。所以清理这类历史数据之前,先确认目标浏览器的现状,别拿今天的支持情况去否定当年的决定。泰语与另两门东南亚语言的差别另见越南泰国印尼在语言层为什么一处都不能共用

粘贴动作是这类字符的主要来源

并不是所有零宽字符都是前端主动插的,很多是被带进来的。

从设计稿、从文档、从翻译工具里复制文本,会连着不可见字符一起复制。

运营在后台粘贴商品描述的时候,这些字符就进了数据库。

这条来源比前端主动插入更常见,也更难管,因为它不走代码评审。

而且它是零星发生的,同一批商品里有的有有的没有。

这条来源的处理办法只有一个,就是在入库那一层做过滤。让后台的保存动作自动剥掉这一族字符,比事后清洗省事一个数量级,因为它把问题挡在了唯一一个所有内容都必经的地方。

这条来源还有一个特别难查的变体,就是翻译服务商交付的文件。译文经过多个工具流转,每一道都可能留下自己的痕迹,而交付验收通常只看语言质量不看字节。把字符检查加进交付验收的清单里,比事后清洗划算得多。翻译交付的验收另见小语种母语审校的验收清单怎么列

泰语那半边的细节交给分词那一篇

零宽空格在泰语上还牵涉到关键词表怎么建、日志怎么清洗。

那些内容属于分词问题的范畴,跟本文讲的排版污染是两条线。

本文关心的是这个字符怎么进来的、会污染哪些字段、怎么扫出来。

它在泰语、日语、中文、高棉语上的表现是同一个机制。

所以处理办法可以共用一套,不需要按语言各写一份。

这个划分也解释了为什么本文把德语和泰语放在一起讲:两门语言的语言学问题完全不同,一个是复合词太长,一个是没有词边界,但它们逼出来的工程修补是同一个动作——往字符串里塞一个看不见的字符,于是产生的下游问题也完全一样。

这个共通性还带出一个实用的推论:你不需要为每门语言各写一套检查,一套字符层的扫描就能覆盖所有语言。真正需要按语言分别处理的是解法那一侧——德语靠断词词典,泰语靠分词算法,而检测那一侧完全可以共用。

把检测和解法拆开还有一个组织上的好处:检测可以由一个人一次性做完并且长期复用,解法需要按语言找不同的人。混在一起排期的话,整件事会被最难的那门语言拖住,而检测本来当天就能出结果。

这些字符怎么检测出来?

一条正则就能扫全站

需要扫的字符是有限的几个,写成一个字符类就够。

软连字符、零宽空格、零宽连接符、字节顺序标记,加上不换行空格。

把这几个码位放进一个字符类,在全站的文本字段里扫一遍。

扫描本身不到一分钟,麻烦的是决定扫哪些字段。

最少要覆盖商品名、标题标签、描述、分类名和筛选项取值。

不换行空格值得单独说一句,它是这几个里唯一有宽度的,肉眼能看出是个空格但看不出它跟普通空格的区别。它造成的问题跟零宽字符一样:字符串比对不相等,而你完全看不出为什么。

扫描的时候建议把结果按字段分组统计,而不是只给一个总数。哪个字段脏得最厉害,通常就指向了某一个具体的操作习惯或者某一个具体的导入通道,顺着这条线往回找,往往一次就能把源头堵掉。

扫描完成之后建议把结果存一份基线,后面每次巡检跟基线比差值。绝对数量本身意义不大,因为总有一些是有意插入的,真正需要关注的是这个数字有没有在悄悄增长。

数据库层怎么查

直接在数据库里查比导出来查更快,也更容易重复执行。

多数数据库支持按十六进制匹配,可以直接找特定字节序列。

要注意字符集,同一个字符在不同编码下的字节序列不一样。

查之前先确认这张表的编码,别在这一步上白折腾半小时。

查出来的结果最好带上主键,方便后面定位和批量处理。

建议把这条查询存成一个固定的脚本,纳入每周的巡检。这类字符是持续流入的而不是一次性的,做一次清洗解决不了问题,需要的是一个能反复跑的检查。

如果没有直接查库的权限,退一步可以从站点地图里逐页抓取标题和结构化数据来扫,覆盖面稍窄但足够发现问题。这条路的额外好处是它扫的是线上真实输出,能顺带发现那些在渲染环节被加进去的字符。

导出关键词表时的清洗顺序

清洗要放在导出之后、比对之前,这个顺序不能颠倒。

先清洗再导出的话,你就不知道原始数据里到底有没有问题。

正确的做法是导出原样,另存一份清洗后的,两份都留着。

比对用清洗后的那份,排查问题用原样那份。

两份的差异条数本身就是一个有用的数字。

这个数字可以直接当作数据质量指标汇报。它不需要解释技术细节,一句话就能说明白:我们的商品名字段里有多少条含有看不见的字符,这个月比上个月多了还是少了。

还有一个细节是清洗规则本身要版本化。今天你剥掉五个字符,半年后有人发现还有第六个,如果没有记录哪一批数据是用哪一版规则清过的,你就说不清一份旧导出该不该重跑。

还有一个实务上的建议是把清洗前后的差异条目单独导一份出来给人看一眼。多数时候扫出来的都是噪音字符,但偶尔会混进一两条是有意插入的,那种如果被一起清掉,线上立刻就会出现溢出。

为什么不该在存储层清洗

直觉上最省事的做法是在入库时把这些字符全部去掉。

但页面渲染那一侧确实需要它们,去掉之后布局又会撑破。

所以正确的分层是存储原样,输出时按用途分别处理。

给浏览器的保留,给结构化数据和数据源的剥掉。

这样两边的需求都满足,也不会出现改一处影响另一处的情况。

不过这条有个前提,就是这些字符确实是有意插进去的。如果它们是从粘贴动作里混进来的噪音,那入库时就该直接剥掉,因为它们本来也没有排版价值。判断该不该在存储层清洗,看的是这个字符是不是被有意加进去的,有意的保留分层,无意的直接过滤。

存储原样还有一个附带的好处,就是它保留了证据。哪天要追查某个字符是什么时候进来的、是谁的操作带进来的,只有原样数据能回答这个问题,清洗过的库里这条线索已经没有了。

分层处理的前提是链路上真的有一个可以插手的输出层。很多老系统的模板直接从数据库取值往页面上打,中间没有任何可以加过滤的地方,那种情况下只能退回去做存储层清洗,同时接受布局要另想办法。

CSS层的解法能覆盖到哪一步?

overflow-wrap管的是什么

这个属性告诉浏览器,当一个词放不下的时候允许强行断开。

它是兜底手段,保证的是不溢出,不保证断得好看。

断点可能落在一个奇怪的位置,读起来不自然。

但对比横向滚动条,断得不好看是个可以接受的结果。

它的最大优点是不需要知道语言,对任何文本都有效。

具体行为可以查MDN文档中overflow-wrap属性的说明,那里区分了几个取值的差别,其中一个只在没有其他断行机会时才生效,正好符合兜底的定位。

需要提醒的是这个属性对某些语言的效果有限,尤其是那些本来就没有空格的语言。它解决的是一个长词放不下的情况,而不是一整句话没有任何断点的情况,后者仍然要靠别的手段。

另外这个属性只作用于它所在的容器和后代,所以要覆盖全站得想清楚挂在哪一层。挂在最外层最省事但影响面最大,挂在每个组件上更精确但容易漏,多数团队最后会选一个折中的中间层。

word-break为什么更危险

这个属性的某些取值会让浏览器在任意字符之间断行。

对中日韩文本这是合理的,因为那些语言本来就可以任意断。

对拉丁字母文本这会让词被随意切成两半,可读性很差。

更糟的是它会作用在整个容器上,不区分里面是什么语言。

多语言站上一旦全局用了它,别的语言就会一起遭殃。

各取值的适用范围可以查MDN文档中word-break属性的说明。实践上的建议是别在全局样式里用它,需要的话按语言选择器限定作用范围,这样才不会伤到别的语言。

一个常见的误用是为了修某一个页面的溢出,直接把这个属性写进了全局样式表。当时确实修好了,半年后有人报告英文页面的单词被切成两半,排查半天才发现是当初那一行。全局样式里的语言相关属性,几乎总是会在别的语言上出事

容器宽度才是根因

前面这些属性都是在处理症状,根因是容器给的宽度不够。

如果一个组件在德语下总是溢出,最好的解法是把它做成弹性的。

让宽度跟着内容走,或者给一个更宽的下限。

这个改动比调断行规则更彻底,也不需要为每种语言单独配置。

代价是设计稿要重新对一遍,这通常是阻力所在。

值得注意的是,把断行规则调好只是让文字不溢出,它不会让一个两行高的卡片装得下四行文字。真正的容量问题还是要在版式上解决,断行属性只能保证不出现横向滚动条这一条底线。

把根因和症状分清楚还有一个好处,就是它决定了谁来改。容器宽度是设计和产品的事,断行属性是前端的事,字符层是内容和数据的事。三件事混在一起讨论,通常的结果是谁都觉得该别人改。

另一个常见的分工误区是把这件事整体丢给前端。前端能修的只有中间那一层,容器宽度要产品点头,字符层要内容侧配合,只推给一个角色的结果通常是他选了见效最快也污染最大的那个办法。

还有一档解决的是别的问题

另有一类属性负责让多行文本的每行长度更均衡。

它优化的是观感,不是溢出,两者的目标不一样。

把它当成溢出的解法会失望,因为它不保证不溢出。

它的价值在标题这类短文本上,能让两行的长度看起来更协调。

在小语种上它的效果比英语更明显,因为词长差异更大。

把这三档分清楚很有用:一档保证不溢出,一档保证断得合语法,一档保证看着舒服。三者可以叠加使用,但不能互相替代,混淆它们的职责是很多样式反复调不好的原因。相关规范可以参考CSS文本模块三级规范

这一档在多语言站上还有一个值得注意的地方:它的效果好坏取决于文本的词长分布,词长差异越大效果越明显。德语这类语言用了之后观感提升很直接,而中文日文这类每个字宽度相同的语言几乎看不出差别。

要不要用这一档,判断标准是这段文字会不会被读者当成一个整体来看。标题、卡片名、按钮文案属于这一类,正文段落不属于,正文本来就是连续阅读的,行长是否均衡几乎无人察觉。

连字断词功能为什么在你的站上不生效?

lang属性没写对是最常见的原因

自动断词功能必须知道文本是什么语言才能查对应的词典。

知道的唯一途径就是元素上的语言属性。

属性没写、写错、或者写在了错误的层级,功能就静默失效。

静默这一点很关键,它不会报错,只是不断词。

于是你会以为浏览器不支持,实际上是它不知道该用哪本词典。

语言属性的正确写法可以查MDN文档中lang全局属性的说明,以及W3C国际化问答中关于在HTML里声明语言的一篇。后者还讲清了页面级声明和局部声明的关系,多语言混排的页面尤其需要看这一段。

还有一种更隐蔽的写法错误是语言属性写在了外层,而内层某个组件被脚本重新渲染时丢掉了继承关系。这种情况在单页应用里比较常见,页面初次加载没问题,交互之后就静默失效了。

还有一种情况是属性写对了但值不规范,比如写了一个非标准的语言标签。浏览器匹配不上就退回默认行为,表现跟没写一样。写值的时候照着标准的语言子标签来,别自己造。

浏览器的连字词典覆盖有限

就算语言属性写对了,功能也要看浏览器带没带这门语言的词典。

主流语言普遍支持,小语种的覆盖情况参差不齐。

而且不同平台上的同一个浏览器,覆盖范围可能不一样。

所以这个功能只能当成锦上添花,不能当成唯一的方案。

兜底那一层必须用不依赖语言的属性来保证。

属性本身的支持情况和取值可以查MDN文档中hyphens属性的说明。实际部署的时候,正确的组合是自动断词打开、兜底属性也打开,前者负责好看,后者负责不出事。

判断某门语言能不能指望这个功能,最简单的办法是拿一个长词在目标浏览器里试一次。这比去查支持表更可靠,因为支持表往往滞后,而且不区分同一浏览器在不同操作系统上的差异。

字体跟断词没有关系

排查这类问题时经常有人怀疑是字体的问题。

字体决定的是字符长什么样,不决定在哪里可以断开。

换字体解决不了溢出,也解决不了断词不生效。

唯一相关的地方是字宽,字体宽一点内容就更容易溢出。

但那是宽度问题,跟断行规则是两件事。

把这条说清楚能省掉很多来回。排查顺序应该是先看容器宽度、再看断行属性、再看语言属性,字体排在最后而且基本不会是答案。

唯一需要注意字体的场合是字形缺失,也就是这门语言的某些字母字体里没有。那会导致显示成方框,但那是另一类问题,跟断行没有关系。字形覆盖的问题另见网页字体在小语种站上占掉的首屏成本

还有一个相关但常被混为一谈的现象是等宽与非等宽字体下的排版差异。等宽字体会让长词占得更宽,更容易触发溢出,但它同样不改变断点位置,所以它影响的是什么时候溢出,不是从哪里断开。

怎么验证它到底生效没有

最快的验证办法是把容器宽度调到很窄,看长词有没有被断开。

断开且带连字符,说明自动断词生效了。

断开但没有连字符,说明生效的是兜底那一档。

没断开直接溢出,说明两档都没生效。

三种结果对应三种不同的排查方向,一眼就能分清。

这个办法的好处是它不需要任何工具,把浏览器窗口拖窄就能看。建议把它写进上线前的检查清单,每种语言各测一个最长的商品名,三分钟能测完。

拖窄窗口还能顺带看出另一件事:这个组件在极窄宽度下的整体表现。很多溢出问题其实是布局在窄屏下没有正确折叠造成的,跟断行无关,而这个测试能一次性把两类问题都暴露出来。

如果需要给别人看证据,把窄宽度下的截图和正常宽度下的截图并排放。这种对比图比任何文字描述都有说服力,尤其是在需要说服设计师改容器宽度的场合。

哪些位置绝对不能出现不可见字符

标题、地址和结构化数据三处零容忍

标题标签的内容会被搜索引擎当成一个整体理解。

地址里出现这类字符会被编码成一串百分号序列,很难看也容易出错。

结构化数据的字段值会被机器直接读取,不经过任何渲染。

这三处的共同点是它们的读者只有机器,不存在排版需求。

既然没有排版需求,插进去的字符就纯粹是负担。

要在这三处保证干净,最可靠的做法是在生成它们的那一步统一过滤,而不是指望上游字段本身是干净的。过滤函数写一次,三处共用,成本极低。地址里的字符编码问题另见小语种URL用本地字母还是拉丁转写

这三处还有一个共同的特点,就是它们都是被复制到别处的源头。标题会被复制到分享卡片,地址会被复制到外链,结构化数据会被复制到各种聚合服务,一处脏了就会被复制到十几个地方去。

另一类值得一起纳入零容忍范围的是那些用来做匹配的属性字段。它们跟标题一样只有机器读,而且一旦带上脏值,筛选结果的数量就会莫名其妙地对不上。属性字段里的另一类语义陷阱另见无糖和含糖在小语种词表里只差三个字母

关键词表和日志也要保持干净

关键词表如果混进了这类字符,去重和分组都会出错。

日志里混进去会让同一个查询被统计成两条不同的记录。

两者的后果都是数据不准,而且不准的方式很隐蔽。

清洗这两处的成本很低,导入的时候加一行处理就行。

不清洗的代价是后面所有基于这些数据的结论都要打折扣。

顺便提一句,做跨语言的数据合并时这一步尤其重要。不同来源的数据用了不同的不可见字符习惯,合并的时候会产生大量看起来重复实际不相等的行,而这种行在人工核对时几乎不可能被发现。

日志这一侧还有个具体的表现值得记住:同一个查询词因为不可见字符被拆成两条记录之后,两条的量都不够高,于是在按量排序的报表里双双沉底,你会以为这个词没什么人搜。拆分不只让数据不准,它还会让一个重要的词整个消失在视野之外

可以放的地方只有正文里的长词

剩下唯一合适的位置是正文段落里那些确实很长的词。

那里的读者是人,排版效果有实际价值。

而且正文文本一般不会被当成标识符去做精确比对。

就算被索引进去,一个正文词的影响也远小于标题。

所以这个位置的收益大于风险,可以放。

不过更好的选择仍然是用样式层解决。只有在样式层确实做不到、而那个词又确实会撑破布局的情况下,才退而使用字符层的办法,并且要在文档里记一笔为什么这么做。

还有一个判断标准是这段文本会不会被当成标识符使用。正文段落基本不会,而商品名、分类名这些会被拿去做匹配和聚合的字段就必须干净,哪怕它们看起来也只是给人读的文本。

另外正文里用这个办法之前,先确认这一段会不会被摘出去当成摘要或者卡片描述。一旦被摘走,它就离开了正文语境进到了一个只有机器读的位置,原来的收益大于风险的判断也就不成立了。

一套断行方案的选型顺序

四步选型

第一步,先看容器能不能加宽或者做成弹性的,能就直接解决。

第二步,加上不依赖语言的兜底断行属性,保证不出现横向滚动。

第三步,把语言属性写对,打开自动断词,让断点尽量落在合理位置。

第四步,只对仍然放不下的极少数词,才考虑往字符串里插东西。

四步走完,需要动字符串的情况通常一个都不剩。

这个顺序的逻辑是从污染范围最小的手段开始试,一步一步往上加,只有前一步真的解决不了才用下一步。绝大多数团队的问题是直接跳到了第四步,因为那是最快看到效果的一步。

四步里最容易被跳过的是第三步,因为它需要改模板加语言属性,而这件事看起来跟断行没有直接关系。跳过它的代价是自动断词永远不会生效,你的页面永远停留在能看但不好看的状态。

四步里第一步的性价比最高但推动最难,因为它牵涉设计改稿。越靠前的步骤解决得越彻底,也越难推动,而越靠后的步骤越容易做,留下的隐患也越多,这条规律在很多技术选型上都成立。

一张能贴在评审清单上的表

表要有四列:手段、作用层、污染范围、允许使用的位置。

作用层这一列只有三个取值,样式、标记、字符。

污染范围写清楚它会影响哪些下游字段。

允许使用的位置直接写页面区域名,别写抽象描述。

这张表贴在前端的代码评审清单里,比写在文档里有用得多。

做这张表的时候记得把不换行空格也列进去。它经常被当成一个排版技巧而不是一个字符,很多团队的规范里根本没有提到它,结果它是这几个里出现频率最高的一个。

表里还建议加一行说明谁有权批准使用字符层的手段。不设这个门槛的话,某个赶工期的下午总会有人直接往字符串里塞点东西,而那种改动一旦上线就很难被发现,更别说被撤回。

表做完之后建议在团队里过一遍,重点不是让大家记住内容,而是让大家知道有这么一张表存在。真正会去查它的场合是某个人正准备往字符串里塞东西的那一刻,那时候他得想得起来有这么个东西。

换行规则本身在不同语言里差在哪?

有空格的语言和没空格的语言

有空格的语言,断行机会天然存在,问题只在词太长。

没有空格的语言,断行机会要靠算法或者人工产生。

前者的解法偏排版,后者的解法偏文本处理。

两类语言的投入完全不同,不能用同一套方案覆盖。

混排页面上两类语言同时出现时,还要注意规则会互相影响。

另有一类语言介于两者之间,比如藏文和高棉文,它们有词但空格的用法跟拉丁语系不同。这类语言的处理需要单独确认,别想当然地归到某一边。跨书写系统的整体情况另见印度十一门语言分摊在九套书写系统上的预算怎么算

混排页面上还有一个具体的坑:中英混排时英文单词是一个不可断的整体,而中文可以任意断,于是一段混排文本的断点分布会很不均匀,行末容易出现大片空白。这跟溢出是两个问题,但经常被一起报上来。

同一套算法在不同语言上的落点

断行算法本身是跨语言通用的,它按字符类别决定哪里能断。

每个字符属于哪一类,是在字符属性表里定义好的。

所以不同语言的差别不在算法,在它们用了哪些字符。

标点符号的类别定义尤其重要,中日韩的标点有专门的规则。

这些规则的目的是避免行首出现不该出现的标点。

完整的规则和字符类别定义在Unicode标准附件14断行算法里,中文版的通俗解释可以看W3C国际化文章断行的方法。后者用图示讲了不同书写系统的断行差别,是这一族资料里最好读的一份。

还有一类字符的类别定义容易被忽略,就是各种破折号和连接号。它们看起来相似,断行类别却不同,有的允许在后面断有的不允许,而内容编辑在录入时几乎不可能分清用的是哪一个。

还有一类需要留意的是数字和单位之间的连接。为了不让数值和单位被拆到两行,很多人会插一个不换行空格,这个做法本身是对的,但它同样是往字符串里加字符,同样会被下游读到。

常见问题解答

我们站上没有德语,还需要管这件事吗?

看两个条件。第一个是有没有构词能力强的语言,芬兰语、匈牙利语、荷兰语、瑞典语都会产生很长的词。第二个是有没有不带空格的语言,泰语、日语、中文都属于这一类。两个条件都不满足的话,溢出问题会轻很多,但粘贴带进不可见字符这条来源仍然存在,因为它跟语言无关,只跟内容编辑的操作习惯有关。所以最少要保留入库过滤那一步,其余的可以先不做。换句话说,溢出问题按语言判断要不要做,字符污染问题按操作习惯判断,两条线要分开评估。

怎么在十分钟内判断自己站上有没有这个问题?

两个动作。第一个是把浏览器窗口拖到最窄,逐页看有没有出现横向滚动条,有的话记下是哪个组件。第二个是在数据库里跑一条正则,扫商品名和标题字段里有没有那几个不可见字符,有结果就说明已经有人在用字符层的办法了。两个动作加起来十分钟,结论足够支撑要不要立项。如果两个都干净,这件事可以先放着,加一条巡检就行。两个动作都不需要开发配合,自己就能做完,这是它值得优先跑一遍的原因。

已经插进数据库的那些字符,要不要全部清掉?

先分清来源再决定。前端为了排版有意插的,短期内不要动,因为清掉之后布局会立刻撑破,正确顺序是先把样式层的方案做好,再清字符。从粘贴动作混进来的噪音字符可以直接清,它们没有任何作用。区分的办法是看位置:出现在长复合词中间的多半是有意的,出现在词首、词尾或者标点旁边的基本都是噪音。清理的顺序永远是先补上替代方案再动原有数据,反过来做会让线上立刻出问题。

软连字符会不会直接影响排名?

直接影响很难证实,也没必要在这个层面纠结。真正确定的影响是链路上的其他环节:你的关键词核对会出错、去重脚本会漏、结构化数据会带上脏值、导给平台的数据源可能被拒。这几件事每一件都会实实在在地消耗时间或者造成损失,理由已经足够充分。把精力放在这些可验证的后果上,比争论搜索引擎到底怎么处理这个字符更有意义。把讨论从推测搜索引擎的行为,转到可以直接验证的下游后果上,推动起来会顺利得多。

自动断词打开之后,需要担心断点断错吗?

正常情况下不用,因为浏览器用的是这门语言的连字词典,断点符合正字法。个别专有名词和新造的复合词可能断得不理想,但它出现的概率不高,而且视觉上只是不够优美,不会造成误解。真正需要小心的是把兜底属性用成了主力,那一档是按字符强行断的,断点完全不管语法,出现在正文里会比较扎眼。所以这两档的分工要保持清楚。所以配置的时候要明确区分主力和兜底,别让兜底那一档承担了主要的断行工作。

这件事该归前端还是归SEO?

发现和定标准归SEO,落地归前端。原因是这个问题的后果落在内容和数据侧,前端从他自己的视角看不到那些后果,他只会看到布局修好了。反过来SEO没有能力去改样式和模板。比较有效的协作方式是把那张四列表交给前端,让它成为代码评审的一条检查项,这样不需要每次都由人去提醒。把判断标准写下来,比每次靠沟通要稳定得多。协作的关键是把判断写成检查项,而不是每次都靠某个人记得提醒。

这跟去掉变音符号、大小写转换那些坑是同一类吗?

都属于字符层的动作,但方向相反。去变音和转小写是主动删掉或者改写信息,目的是让匹配更宽松;插不可见字符是主动增加信息,目的是让排版更好看。前者造成的是过度合并,后者造成的是不该有的分裂。两类问题的排查工具可以共用一套,都是在字节层做比对,但监控的指标要分开:前者看误合并率,后者看字段里的异常字符数。一句话概括就是:一个让不同的东西变成相同的,一个让相同的东西变成不同的。

权威参考资料

分享到
标签
版权声明

本文标题:《为了让德语长词在手机上不撑破,前端往标题里塞了个看不见的字符》

本文链接:https://zhangwenbao.com/minor-language-line-break-soft-hyphen-invisible-character.html

版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交