转成小写这一步在英文站上从来不出事,到了土耳其语站它把词改成了另一个词
本文目录
- 为什么把字母转成小写,在英文站上从来不算一个决策?
- 英语的大小写是一张一一对应的表
- 换一门语言,这张表就有了例外
- 函数签名里那个缺席的参数
- 土耳其语的四个字母,怎么把一个词变成另一个词?
- 一个点的有无,是两个独立的字母
- 一个真实的商品词,转完之后不再是它自己
- 带地区参数的版本,为什么也不是万能开关
- 点没了之后,字符个数也变了
- 德语的ß换成两个S之后,为什么再也回不去?
- 一个字符变成两个字符
- 不可逆的那一步在哪里
- 关键词表里两行词,其实是一个市场的两拨人
- 希腊语末尾那个字母,为什么让同一个词在库里存成两份?
- 位置决定字形,这是上下文相关映射
- 两种形态在检索侧算不算同一个词
- URL里的希腊字母还要多考虑一层
- 站内最先出事的四个位置
- slug生成器
- 关键词表去重
- 站内搜索的匹配层
- 正则表达式里的字符类
- 关键词表去重时,大小写折叠会错误合并哪些词?
- 先分清折叠和转小写这两件事
- 哪些合并是对的,哪些是错的
- 合并造成的损失怎么估
- 数据库的排序规则,到底折叠了什么、没折什么?
- 排序规则同时管着比较和排序两件事
- 排序顺序和字母表顺序是两回事
- 缓存键和去重键最容易被忽略
- 重定向和规范化规则,应该在哪一层加语言参数?
- 服务器层做小写化,是最危险的一处
- 应用层做的时候要带上页面语言
- 第三方组件里的那几处,只能绕不能改
- 母语审校清单里要加一条
- 两小时能跑完的大小写自检清单
- 第一步:做一次往返测试
- 第二步:数一遍站内调用点
- 第三步:核对搜索侧的两端
- 哪些问题不归语言层,该交给谁?
- 域名那一半不在这里
- 引擎怎么处理,是引擎层的事
- 变音符号那一半单独走
- 常见问题解答
- 只做英语和几门西欧语言,还需要管这件事吗?
- 怎么在半小时内判断自己的站有没有中招?
- 已经生成的那批错误slug,到底要不要改?
- 数据库排序规则到底该选哪一个?
- 站内搜索应该用折叠规则还是转小写?
- 母语审校能不能发现这类问题?
- 这跟去掉变音符号是不是一回事?
- 权威参考资料
摘要:把字符串转成小写,在英文站是一个不需要讨论的动作,因为英语的大小写是一对一、无损、可逆的。换到土耳其语、德语、希腊语上,同一个函数会改变词义、改变字符串长度、改变字符个数,而它默认用的语言是英语。本文拆开这条链路上的八个落点,给出一份往返测试式的自检办法,两小时就能跑完。
为什么把字母转成小写,在英文站上从来不算一个决策?
英语的大小写是一张一一对应的表
英语字母有二十六个大写、二十六个小写,一个对一个,中间不缺不多。转换过去再转回来,字符串一个字节都不会变。
正因为这个性质太稳,它在工程里被降格成了一个工具函数。没人给它写测试,没人在代码评审里问一句这里是什么语言。
更关键的是,这个动作在英语里是幂等的。转一次和转十次结果一样,所以它可以被放心地塞进任何一层管道。
slug生成器里有它,去重脚本里有它,搜索匹配里有它,重定向表生成里也有它。四个地方各调一次,谁也不会觉得有问题。
于是它悄悄变成了全站调用次数最多、被审查次数最少的一个操作。你在代码里搜一遍,通常能搜出上百处。
这就是问题的起点:一个操作之所以从来不出事,可能不是因为它安全,而是因为它一直只在一种语言上被使用。英语站上的十几年经验,验证的其实是英语这门语言的一个巧合性质——它的正字法碰巧不需要大小写映射表里出现任何例外条目,而世界上绝大多数使用拉丁字母的语言都做不到这一点。
换一门语言,这张表就有了例外
Unicode给大小写映射准备了两份文件。一份是简单映射,一个码位对一个码位;另一份专门收例外。
例外文件的存在本身就说明了问题:如果一一对应到处成立,就不需要单独开一个文件来收特例。
例外分三类。第一类跟语言有关,同一个字符在不同语言里往不同方向变;第二类跟上下文有关,字符在词尾和词中变得不一样。
第三类最麻烦,它改变字符串的长度。一个字符转成大写变成两个字符,字符数从此对不上了。
这三类分别对应土耳其语、希腊语和德语,都不是什么冷门语言,加起来的市场规模不算小。
把这三类例外摆在一起看,会发现一件反直觉的事:大小写转换根本不是字符级操作,它是词级操作,甚至是语言级操作。字符级的直觉之所以能在英语上活下来,是因为英语恰好没有一条规则需要看邻居、看语言、看词的边界,而其他语言至少要看其中一样。
函数签名里那个缺席的参数
大多数语言的标准库提供两个版本:一个不带地区参数,一个带。不带的那个,用的是某个默认地区。
默认地区往往取自运行环境,也可能被硬编码成不区分地区的根地区。无论哪种,都跟你页面上那门语言无关。
这里有一个判据可以直接搬走:一个函数如果没有语言参数,它不是通用的,它是选了默认值的。
而默认值几乎总是英语,或者是一套按英语的假设设计出来的规则。它不会报错,也不会警告,只是安静地按另一门语言处理你的词。
这跟编码问题的手感完全不同。编码错了会出现乱码,人一眼能看见;大小写错了出来的是一个长得很正常的词。
更麻烦的是,出错的那个结果通常也是一个合法的字符串,能存进数据库,能拼进网址,能在页面上正常显示。它唯一的问题是,用户在搜索框里打的不是这一串。这类错误不会触发任何一条告警,只会在半年后表现为某几类词的流量一直起不来,而排查的人根本不会往字符层想。
土耳其语的四个字母,怎么把一个词变成另一个词?
一个点的有无,是两个独立的字母
土耳其语的拉丁字母表里,带点的i和不带点的i是两个字母,各有各的大小写形态,一共四个码位。
带点的小写i,大写是带点的大写;不带点的小写,大写才是我们熟悉的那个光秃秃的大写字母。
换句话说,在土耳其语里,把大写字母转成小写,得到的不是英语里那个i,而是不带点的那一个。方向整个反了过来。
这四个码位是U+0049、U+0069、U+0130、U+0131。前两个是英语也在用的,后两个是土耳其语专有的。
它们在字体里经常缺,在键盘上分两个键,在搜索框里被用户随手替换成ASCII写法。这三件事互相叠加。
需要强调的是,这不是变音符号那种可加可不加的装饰。带点和不带点在土耳其语里区分词义,就像英语里的b和d一样,是两个字母,不是一个字母的两种写法。把它们当成同一个字母折叠掉,等于在英语词表里把bad和dad合并成一条。
一个真实的商品词,转完之后不再是它自己
拿一个卖婴儿浴巾的土耳其语站举例。湿这个形容词写作ıslak,开头是不带点的那个字母。
把它整词大写用作导航标签,得到的第一个字母是光秃秃的大写。到这一步还没问题。
问题出在下一步:把这个大写串按默认规则转回小写,出来的开头变成了带点的i。词形从此变成了另一个串。
这个新串在土耳其语里不是一个词。它进了关键词表,进了slug,也进了站内搜索的索引。
用户当然搜不到。后台看上去一切正常,词有排名有展现,只是点击量长期偏低得不像话。
另一个方向更热闹一点。土耳其语里有一个非常常用的词,意思是频繁、密集,开头也是不带点那个字母。用默认规则把它的大写形态转成小写,会得到一个在英语拼写里完全合法、但在土耳其语语境里绝对不适合出现在婴幼儿商品页上的词。这个例子在软件工程圈里流传了二十年,只是几乎没人把它当成一个搜索问题来讲。
带地区参数的版本,为什么也不是万能开关
标准库确实提供了带地区参数的版本,传入土耳其语的地区码,映射方向就对了。
但这只解决了你自己代码里的那一处。链路上还有数据库、搜索引擎、模板引擎、表格软件、第三方插件。
它们各有各的默认值,各有各的配置位置。有的能配,有的只能整库配,有的压根没有这个概念。
这就回到了一条老经验:支持是链条属性,不是节点属性,能不能用取决于最差的那一环。
所以正确的做法不是到处传参数,而是先画链路,标出每一处发生了大小写转换的位置,再决定哪几处必须传、哪几处应该干脆不做这个转换。
还有一种更省事也更稳的思路:在这门语言上,把大小写转换从关键路径上整个拿掉。索引存原形,匹配用专门的折叠规则,展示层交给样式表去控制视觉上的大写效果。这样做需要改的地方比逐处传参数少,出错的面也小得多。
还有一个容易被忽略的现实:很多团队的多语言站是同一套代码跑十几门语言,转换函数被抽成了公共库。公共库天然倾向于不带参数的那个版本,因为带参数意味着调用方必须提供语言,而调用方常常拿不到。ECMA-402国际化规范把这类地区敏感行为单列成一份标准,本身就说明它不是可以顺手带过的细节。
点没了之后,字符个数也变了
还有一个更隐蔽的形态。把带点的大写字母按不区分地区的规则转成小写,某些实现会得到一个普通i加上一个单独的组合点,两个码位。
肉眼看它跟一个普通i几乎没有区别,但字符串长度多了一位,字节数多了两位。
这个串跟用户输入的串在字节层面不相等。任何等值比较都会返回否,任何哈希去重都会把它当成新词。
它还会污染统计。关键词表里两行看着一模一样,搜索量一分为二,谁也不够门槛。
做规范化的时候如果没有先做Unicode规范化,这一对就会一直并存下去,越积越多。
这类问题的诊断办法很朴素:把两个看起来相同的字符串各自打印出字符个数和码位序列,一比就知道。真正难的不是诊断,而是意识到要去诊断——因为屏幕上它们长得一模一样,不会有任何人主动怀疑这两行是两个东西。
组合点这一类问题的根子在于同一个视觉结果可以有多种码位写法,而字符串比较看的是码位不是视觉。规范化就是把多种写法统一到一种上去的过程,Unicode字符数据库标准附件里把每个字符的分解方式都标了出来,需要的时候可以直接查表核对。
德语的ß换成两个S之后,为什么再也回不去?
一个字符变成两个字符
德语的ß长期没有官方的大写形态。传统规则是遇到全大写就写成两个S。
于是Straße全大写之后是STRASSE,五个字母变成六个,一个码位变成两个。
这是大小写转换里唯一一类会改变长度的操作,也是最容易在字段长度、截断、对齐上出问题的一类。
标题在搜索结果里按像素截断的时候,这多出来的一个字母有时候正好把最后一个词挤出去。
更常见的是列表页的对齐,全大写的德语商品名比原形长出一到两个字符,本来排得整齐的三列就歪了。
2017年之后,德语正字法承认了ß的大写形态,允许在全大写时保留它。但这只是允许,不是强制,两种写法在同一个市场上并存,也就意味着你的关键词表里天然会有两套形态,而不是一套换成另一套。
字段长度也要跟着重算。如果你按英语经验给标题字段留了固定的字符数上限,德语全大写之后可能正好越界,写入时被静默截断,页面上就出现半个词。这类截断不会报错,只会在某一批商品上表现为标题少了一截,而排查的人往往先去怀疑模板。
不可逆的那一步在哪里
关键不在于变长,而在于变完之后信息丢了。看到STRASSE,你无法判断它原来是Straße还是Strasse。
两个不同的词在大写这一步被合并成同一个串。再转回小写,只能得到strasse,另一个词永远回不来了。
这就是有损转换。它跟变音符号折叠很像,区别在于变音符号折叠是你主动选的,你知道自己在做取舍。
大小写转换没人觉得自己在做取舍。它被当成一个格式化动作,跟去掉首尾空格放在同一类。
一旦有损转换发生在存储环节而不是展示环节,原始信息就没了,后面再补救就要重新拿源数据。
这里可以提炼出一条通用判据:凡是会改变字符串长度的转换,一定是有损的,绝对不能发生在存储层。长度不变的转换也可能有损,但长度一变,几乎不用再看第二眼。
判断一步转换是不是该放在存储层,有个很朴素的问法:这一步做完,我还能不能把原来的东西还原回来。答得出来就放心存,答不出来就只能当成派生数据,原形必须另存一份。这条判断标准跟数据库设计里不存冗余的直觉正好相反,很多事故就出在为了少存一列而丢了原形。
关键词表里两行词,其实是一个市场的两拨人
Straße和Strasse在德语区不是错别字关系。瑞士德语正式废除了ß,全部写成两个s。
所以同一个词,德国和奥地利用户打一种,瑞士用户打另一种,两边都是正确拼写。
如果在建表阶段就把它们折叠成一行,你等于把瑞士市场的搜索量并进了德国的桶里。
合并之后总量确实更好看,但你再也算不出瑞士那一侧值不值得单独做落地页。
这跟葡语两个市场、西语两侧的分叉是同一类问题,只是这次的分界线落在了一个字符上。
处理办法跟做德语复合词与变音符号选词时那套一样:两种形态都保留在词表里,各自算量,在页面上二选一做主形态,另一种进内容里自然出现一次。别指望引擎替你把这两拨人合并,也别自己动手合并。
顺带提一句瑞士市场的另一层特殊性:那里同时有德语、法语、意大利语三个语言区,你为德语做的这个决策不会自动适用于另外两个区。把国家当成语言的容器几乎总会出错,正确的做法是先把语言和市场拆成两个字段,再决定哪一层做分叉。
希腊语末尾那个字母,为什么让同一个词在库里存成两份?
位置决定字形,这是上下文相关映射
希腊语的sigma有两个小写形态。词中用一个,词尾用另一个,大写只有一个。
把大写词转成小写的时候,实现必须判断这个字母是不是在词尾,才能选对形态。
这就是上下文相关映射:同一个大写字母,转成小写可能得到两个不同的结果,取决于它旁边是什么。
能正确处理这一条的实现不算少,但简单映射表做不到,正则替换更做不到,Unicode官方问答里关于完整映射与简单映射的区分把这一点讲得很直白。
做不到的后果是:词尾出现了本该只在词中用的那个形态,希腊语用户一眼就能看出来这页不是本地人写的。
三个形态分别是 Σ、σ、ς,这也是本文里唯一一组能在图上原样画出来的字母——土耳其语和德语的那几个特殊字符在很多字体里干脆缺失,反而是希腊语这一组几乎哪都有。这个细节本身值得记一笔:字形覆盖率和大小写规则复杂度之间没有任何关系,你不能靠字体正常显示来推断这门语言的转换规则简单。
两种形态在检索侧算不算同一个词
好消息是,主流搜索引擎在希腊语上会把这两个形态当成同一个词处理,用户搜哪种都能命中。
坏消息是,你自己的站内搜索大概率不会,除非你显式配了对应的折叠规则。
Unicode的大小写折叠表里专门为这一对准备了条目,把它们都折向同一个目标形态。
这也解释了为什么折叠和转小写不是一回事。折叠是为了比较,转小写是为了显示,两者的目标不同。
混用的结果就是:拿显示用的结果去做比较,或者把比较用的结果直接展示给用户看。
把这两件事分开,是整篇文章里最省事也最值钱的一条改动。存原形用于展示,另存一份折叠形态用于比较和去重,两列并行,谁也不覆盖谁。改动量通常只有一个字段加一次批量回填。
更值得注意的是折叠规则本身也在演进。Unicode每一次大版本都可能新增或调整条目,而你的运行时依赖的库版本未必跟得上。ICU国际化组件库是多数系统实际使用的实现,升级时最好把探针词表重跑一遍,看看有没有条目变化影响到已有的比较键。
URL里的希腊字母还要多考虑一层
如果slug用的是希腊字母,那么大小写规则会和网址的规范化规则叠在一起发生。
网址的路径段区分大小写,域名不区分,这两半本来就走的是两套机制。
再叠上一个上下文相关的字形选择,同一个词就可能生成两条不同的路径。
两条路径都能打开,都能被抓取,都能被外链指到,收录里就多了一份自我竞争。
这一层的完整拆解在小语种URL用本地字母还是拉丁转写里已经写过,本文只补一句:大小写这一步要放在转写之前做,顺序反了会多出一批孤立的旧地址。
另一个常被忽略的细节是外链。别人链接到你的希腊字母地址时,多半是从浏览器地址栏复制的百分号编码形态,粘出来的大小写和你的原始形态未必一致。这批外链会落到一个跟规范地址不完全相同的路径上,权重能不能合过来,取决于你的规范化标签配得对不对。
站内最先出事的四个位置
slug生成器
slug生成器是全站最爱调用小写函数的地方,通常还紧跟着一步去掉变音符号。
两个有损操作连着做,出来的字符串跟原标题的关系已经很难反推。
更糟的是这一步的结果会被写死进数据库,成为文章的永久地址。
发现问题的时候,改slug意味着改地址,意味着一批301跳转和一段时间的波动。
所以这一处的正确做法是:上线前用目标语言的真实词跑一遍,别等内容都发完了才发现规则不对。
值得单独检查的是那些以特殊字母开头的词,因为首字母出错的slug在字母序里会整个跑到另一个位置去,人工翻列表的时候特别容易漏看。
slug规则改动还有一个隐性成本:站内已有的内链、站内搜索的历史记录、外部工具里保存的地址列表都指向旧形态。一次规则修正往往要配一张映射表,把旧地址逐条对到新地址上去,而这张表越晚做越难做,因为你会越来越不确定某个旧地址当初是怎么生成出来的。
关键词表去重
做词表的人几乎都会先把所有词转成小写再去重,这一步几乎是肌肉记忆。
在英语上它完全正确,在土耳其语和德语上它会把不该合并的词合并掉。
合并之后的表看起来更干净,行数更少,搜索量更集中,汇报的时候数字也更好看。
但你失去的是分叉信息,而分叉信息恰恰是小语种选词里最贵的那一部分。
建议把去重拆成两步:先按原形去完全重复,再单独看大小写差异那一批,人工过一遍。
人工那一遍通常只有几十行,一个母语者十分钟就能标完哪些是真同词、哪些是两个词。这是整条流水线上性价比最高的一次人工介入,比事后从流量数据里倒推便宜太多。
词表流水线上还有一个更早的环节值得检查:从工具导出的原始数据本身可能已经被规范化过一次了。有些平台在返回查询数据前就做了统一小写,你拿到手的已经是加工过的形态。这种情况下再怎么小心也补不回信息,只能换一个能返回原形的数据源,或者用小语种关键词工具没数据时的替代办法里那几种自采集方式。
站内搜索的匹配层
站内搜索要不区分大小写地匹配,就必然要在索引和查询两侧各做一次转换。
两侧用的必须是同一套规则,而且必须是折叠规则,不是显示用的小写规则。
实践里最常见的故障是:索引侧用了带地区的规则,查询侧用了默认规则,两边对不上。
症状很典型,用户搜完整词能搜到,搜某几个以特殊字母开头的词就一片空白。
排查的入口是把两侧的规范化结果各打印一份出来对照,一比就能看出错在哪一侧。
这类站内搜索的语言层配置,跟土耳其语后缀堆叠的选词处理要一起做,否则词干还原对了、大小写又错了,两个问题会互相掩盖对方的症状。
如果站内搜索用的是现成的搜索引擎组件,那么分析器链条通常由若干个过滤器串起来,小写过滤器只是其中一环。把这条链打印出来,你会发现前后顺序也很关键:先做词干还原再折叠,跟先折叠再做词干还原,在屈折语上得到的结果可能完全不同。
正则表达式里的字符类
写正则的时候,一个方括号加a到z,是最常见的写法之一。
这个范围里没有任何一个带变音符号的字母,也没有不带点的那个土耳其字母。
用它做校验,本地语言的正常词会被判成非法;用它做替换,特殊字母会被整个吃掉。
不区分大小写的标志位在某些实现下还会把土耳其字母匹配进英语字母的范围里,方向再一次反了。
正确的写法是用Unicode属性类,而不是手写字母范围。这一条改起来不难,难在全站搜出所有的手写范围。
校验类正则的危害比替换类更大,因为它直接决定用户能不能提交表单。一个只认英语字母的姓名校验,会把大量本地用户挡在注册流程外面,而这些人不会打客服电话解释,他们只会关掉页面。Unicode标识符与语法标准附件里给出了按属性构造字符类的推荐做法,可以直接照搬。
关键词表去重时,大小写折叠会错误合并哪些词?
先分清折叠和转小写这两件事
转小写的目标是产出一个给人看的字符串,它要符合这门语言的正字法习惯。
折叠的目标是产出一个给机器比较的键,它不需要好看,只需要稳定且一致。
两者的规则表不同,Unicode分别提供了两份数据文件,用途写得很清楚。
混用最典型的后果,是把折叠后的键当成展示文本写进了页面标题。
用户看到的标题就成了一串既不像大写也不像小写、还缺了几个符号的怪东西。
把这两件事在数据模型上分成两列,是一次性投入、长期收益的改动。展示列存原形,比较列存折叠结果,所有的等值判断、去重、缓存键都走比较列,页面上永远只渲染展示列。
命名上也建议区分开。把比较用的那一列直接叫成小写列,几个月后一定会有人拿它去渲染页面,因为名字听起来就像是一个可以显示的东西。叫成匹配键或者比较键,就不太会有人误用,这是个几乎零成本却能长期生效的小防线。
哪些合并是对的,哪些是错的
同一个词的句首大写和词中小写,合并是对的,这是绝大多数场景的诉求。
整词大写的导航标签和正文里的原形,合并也是对的,它们确实是一个词。
土耳其语带点和不带点的那两个字母,合并是错的,它们是两个字母。
德语的ß和两个s,合并要看市场,德奥和瑞士的用户群不同,量要分开算。
希腊语的两个小写sigma,合并是对的,它们是同一个字母的位置变体。
把这五条列成一张小表贴在词表流水线的文档里,比写十页规范管用。判断依据只有一条:合并之后,你还能不能把这两拨人分开算量;分不开还非要合,就是在给自己的下一次决策制造盲区。
这张表还有一个用途:它能直接变成给开发的验收用例。每一行写成一个断言,输入两个词形,期望输出是相等还是不相等,跑一遍就知道当前实现符不符合语言事实。用例的价值在于它把语言学结论固化成了可执行的东西,人员流动之后规则也不会跟着流失。
合并造成的损失怎么估
不用等半年看流量。直接从现有词表里挑出所有含特殊字母的词,数一下有多少行。
再看这些行的搜索量总和占整张表的百分之几,这个比例就是你的风险敞口。
土耳其语站上这个比例通常不小,因为那两个字母在常用词里出现频率相当高。
德语站上比例小一些,但集中在地址、街道、尺寸这类高意图词上。
希腊语站上比例最低,因为词尾形态的问题主要影响长尾而不影响头部。
算完这三个数字,要不要投入就有了依据,也就不必再靠感觉争论。整个过程用表格软件做一次筛选加求和就够了,不需要开发介入。
如果连词表都还没建好,可以退一步用现有的商品名和分类名来估。把站内所有的商品标题拉出来,统计含特殊字母的比例,这个数字跟词表里的比例通常在同一个量级。数据不完美,但足以支撑一次要不要立项的判断,而这类判断拖着不做的代价往往更高。
数据库的排序规则,到底折叠了什么、没折什么?
排序规则同时管着比较和排序两件事
数据库的排序规则名字里通常带着几个后缀,标明它是否区分重音、是否区分大小写。
不区分大小写意味着查询条件会自动做折叠,你在应用层再折一次就是重复劳动。
不区分重音意味着带变音符号的字母和不带的会被当成相等,这个默认经常出乎意料,PostgreSQL文档里关于排序规则同时影响比较与排序的说明值得整节读一遍。
两个不区分叠在一起,土耳其语的两个字母就在数据库层面被当成同一个了。
唯一索引在这种规则下会把两个合法的词判成冲突,插入直接失败,报错信息还看不出原因。
更隐蔽的是,同一张表上不同字段可能挂着不同的排序规则,联表查询时数据库会挑一个来用,结果取决于执行计划。这类问题在测试环境往往复现不出来,因为数据量小的时候执行计划不一样。
字段级的规则设置还有一个实际用途:把需要精确区分的那几列单独挑出来,比如商品编号、优惠码、用户名,给它们挂上区分大小写的规则。这几类值本来就不该被折叠,它们是标识符不是自然语言,混在一起用同一套规则迟早会出现两个不同编码被判成同一个的事故。
排序顺序和字母表顺序是两回事
按默认规则排序,出来的是码位顺序,不是这门语言字母表的顺序。
土耳其语的字母表里,不带点的那个字母排在带点的前面,跟码位顺序正好相反。
结果是品牌列表页、分类字母导航、筛选项列表,全部排得跟当地人的预期不一样。
用户不会去投诉排序,他们只是找不到要找的那一项,然后离开。
解决办法是给排序单独指定这门语言的规则,而不是复用比较用的那一套。
这一条跟北欧三语的字母表排序差异是同一类问题,那边是三门语言把同样三个字母排在三个不同位置,这边是同一门语言里码位顺序和字母表顺序打架。
字母导航这个组件在小语种站上值得单独验一次。它通常按硬编码的英语字母表生成按钮,本地语言多出来的那几个字母要么没有入口,要么被塞在最后。用户找不到以本地字母开头的品牌,就只能退回搜索框,而搜索框那一侧的折叠规则如果也没配对,这条路径就整个断了。
缓存键和去重键最容易被忽略
页面缓存、查询缓存、幂等键,这些地方几乎都会对字符串做一次规范化再算哈希。
规范化规则一旦跟数据库那一层不一致,就会出现命中率异常和偶发的脏数据。
这类问题的表现是间歇性的,只在特定语言、特定词上出现,测试环境很难复现。
排查时先把这条链路上所有做规范化的位置列出来,通常有四到六处。
把它们统一到一个函数上,是治本的做法,也是唯一能保证以后不再复发的做法。
缓存键的另一个风险是跨语言碰撞。如果规范化把不同语言的两个词折成了同一个键,两个页面就会互相覆盖对方的缓存,表现为偶尔刷出另一门语言的内容。这类故障出现频率低、复现困难,通常拖很久才被定位,而根因往往只是缓存键里少带了一个语言标识。
重定向和规范化规则,应该在哪一层加语言参数?
服务器层做小写化,是最危险的一处
很多站在服务器配置里加了一条规则:把所有大写路径301跳到小写路径。
这条规则用的是字节级的转换,只认A到Z,对多字节字符要么不动,要么按错误的方式处理。
对本地字母的路径来说,它要么什么都不做,要么把百分号编码的部分改坏。
改坏之后跳到一个不存在的地址,用户看到的是404,抓取工具看到的是一串死链。
这一处的正确做法是把规则限制在纯ASCII路径上,其余的交给应用层按语言处理。
顺带说一句,如果你的站根本没有大写路径,这条规则就该删掉。为一个不存在的问题保留一条会改坏地址的规则,是典型的负资产。
顺序也很讲究。服务器层的重写规则通常在应用层之前执行,它改坏的地址应用层再也看不到原样。所以这一层的规则要尽量少、尽量保守,能在应用层做的事就别放在这里做,出了问题至少还有日志能还原现场。
应用层做的时候要带上页面语言
应用层知道当前页面是什么语言,这是它相对服务器层唯一的、但也是决定性的优势。
把这个语言值传给转换函数,土耳其语页面就会走土耳其语的规则。
多语言站要注意的是取哪个语言值,页面语言、用户界面语言、内容语言可能是三个不同的东西。
正确的选择是内容语言,也就是这段文本本身是什么语言,跟界面语言无关。
这一点跟页面上语言声明该取哪个值的判断一致,可以参考国际化SEO与hreflang的完整实现清单里关于语言值与地区值分开填的那一节。
怎么把内容语言可靠地传下去,本身就是个工程问题。比较稳的做法是让页面语言在渲染入口处解析一次,之后作为上下文往下传,而不是在每一层各自去猜。W3C国际化问答中关于HTML语言声明的说明里讲清了页面级和元素级两种声明的取值优先级,可以当作传参规则的依据。
第三方组件里的那几处,只能绕不能改
表格软件、翻译记忆工具、外链分析工具、词频统计脚本,各自都会做一次规范化。
它们大多不给你选项,也不会告诉你用了什么规则,你只能在输入输出两端做核对。
核对办法是准备一份包含所有特殊字母的探针词表,每次工具换版本就跑一遍。
这份词表二十行足够,覆盖四个土耳其字母、德语两种形态、希腊语两种词尾形态。
跑完对照原表,哪一行变了就知道这个工具在哪一类上不可靠,可以有针对性地绕开。
这份探针表还有一个额外用处:新同事接手流水线时,让他先跑一遍,比看三页文档更快建立起对这类问题的直觉。
拼写检查类工具是另一个容易被忽略的环节。很多内容平台内置的检查器依赖开源词典,而词典对语言变体的支持参差不齐,Hunspell拼写检查引擎就是其中被用得最多的一个。它把一个正常词标成错误,编辑手动改掉,你的词形就在编辑环节被改坏了,而这一步根本不在任何技术清单里。
母语审校清单里要加一条
母语审校通常盯的是句子读着自然不自然,很少有人让他们去看字符层。
但字符层的错误恰恰是母语者一眼就能发现、而你自己永远看不出来的那一类。
在验收清单里加一条:导航标签、按钮文案、面包屑这些全大写或首字母大写的位置,逐个看一遍。
这几个位置正好是全站最常触发大小写转换的地方,也是最容易出现怪词的地方。
加这一条几乎不增加审校成本,却能拦住绝大多数会被用户看见的错误,相关的验收方法在小语种内容的母语审校验收清单里有完整的表格。
还可以让审校者顺手记录他们改动的类型。如果某一类改动反复出现,那多半不是译者的问题,而是上游某个自动处理环节在持续制造它。把改动类型统计一下,往往能倒推出流水线上具体是哪一步出了错,这比让技术团队自己去找便宜得多。
两小时能跑完的大小写自检清单
第一步:做一次往返测试
拿这门语言的一百个常用词,各做一次转大写再转小写,跟原串逐字节对比。
回不到原串的,就是有损转换命中了。这一步不需要懂语言学,脚本十行就能写完。
德语站上会有一批词回不去,那是ß的问题,属于预期之内。
土耳其语站上如果有词回不去,那就是地区参数没传对,属于必须修的问题。
把回不去的词列出来,按首字母分组,一看就知道是哪几个字母在作怪。
这个测试的价值在于它不需要任何先验知识:你不必知道这门语言有哪些例外,测试会把它们全部指出来。往返不等,就是有损,这一条判据可以直接搬到任何一类字符串转换上去,包括转写、去变音、全半角统一。
往返测试还能顺便测出另一类问题:某些实现在处理特定字符时会抛异常或者返回空串。这类硬故障比词形改变更容易发现,但如果没人跑过测试,它也可能在生产环境里静静躺着,直到某一天有用户提交了一个含特殊字母的搜索词。把往返测试做成上线前的固定一项,是这套改动里最省力的一半。
第二步:数一遍站内调用点
在代码库里搜所有转小写、转大写、折叠、去变音的调用,统计出现次数和位置。
按发生的层次归类:展示层、存储层、比较层、地址生成层。
存储层出现的每一处都要单独讨论,因为那里的有损转换会永久丢信息。
展示层的问题只影响这一次渲染,改起来便宜,优先级可以往后放。
清单做完通常会发现,真正需要动的只有三到五处,其余的都可以维持原样。
统计的时候把调用点按语言分组也很有用。如果某几门语言的页面根本不经过某条链路,那条链路上的问题就可以直接降级。范围收窄之后,需要人工审的位置常常只剩下个位数,排期的时候也更容易说清楚投入产出。
第三步:核对搜索侧的两端
把站内搜索索引侧和查询侧的规范化结果各导一份,用同一批探针词对照。
两侧一致才算过,不一致的话先改查询侧,因为索引重建成本更高。
改完之后拿真实的零结果查询日志复跑一遍,看看能救回多少条。
这个数字通常比预期高,因为零结果里混着一批本来该有结果的查询。
救回来的那部分,是这次改动最容易拿去汇报的成果,也最能说服人继续投入。
零结果日志是这类改动里最好用的验收材料,因为它是唯一能直接看到用户真实输入的地方。改动上线前后各导一份,按查询词首字母分组对比,救回来的那批词通常集中在几个特殊字母上,图表画出来一目了然,比任何解释都有说服力。
哪些问题不归语言层,该交给谁?
域名那一半不在这里
域名不区分大小写,这是协议层的规定,跟语言无关,所以域名上不存在本文说的这些坑。
真正跟域名有关的是本地字符域名的输入和传递问题,那是另一个层面的事。
路径段区分大小写,所以本文讨论的所有问题只发生在路径、查询串和内容里。
这个边界要在团队内部说清楚,否则讨论会一直在两个层面之间来回跳。
把域名那一半从议题里摘出去,剩下的问题范围会小很多,也更容易排期。
还有一类容易混进来的议题是邮箱地址。本地字符邮箱的支持程度跟域名完全是两回事,它涉及的是另一条协议链路,RFC 8264定义的PRECIS框架规定了这类标识符该怎么做大小写与规范化处理。放在这篇里只会把讨论拉散,识别出来交出去就好。
引擎怎么处理,是引擎层的事
搜索引擎自己会做大小写归一,而且在主流语言上做得比你好,这一点不必怀疑。
你要保证的是自己产出的字符串是对的,而不是去猜引擎的归一规则。
换句话说,这一层的目标是不制造错误的词,而不是去优化引擎的匹配行为。
两者混淆的表现,是有人开始为了迎合猜测中的引擎行为去改正确的拼写。
那是本末倒置,本地用户会先于引擎发现你的页面写得不像人话。
反过来说,引擎的归一能力也不是无限的。它在有充分语料的语言上做得好,在语料稀薄的语言上未必,而这恰恰是小语种站最需要自己兜底的部分。判断办法很直接:拿两种词形各搜十次,看结果页是不是同一批,是就不用管,不是就得自己在站内补一层。
变音符号那一半单独走
去变音符号和转小写经常写在同一个函数里,但它们是两个决策,应该分开评估。
去变音在有些语言上是必须的用户覆盖手段,在有些语言上是纯粹的信息损失。
这一层的判断依据是用户实际怎么输入,而不是正字法怎么规定。
相关的判断框架在希腊语用户用拉丁字母打字的覆盖问题里讲得比较完整。
本文只强调一点:这两个动作绝不能捆在一个函数里,否则你永远没法单独关掉其中一个。
把这两个动作拆开还有一个附带好处:你可以分语言开关。土耳其语站关掉去变音、开着大小写的地区规则;希腊语站两个都开;德语站只在比较键上折叠、展示层一律保留原形。同一套代码三种配置,这才是多语言站该有的形态,而不是一套写死的规则跑遍全球。
常见问题解答
只做英语和几门西欧语言,还需要管这件事吗?
要管的部分比想象中多。法语、西班牙语、意大利语的大小写映射确实是一一对应的,不会出现土耳其语那种方向反转,但它们都带变音符号,而去变音这一步经常跟转小写写在同一个函数里。只要那个函数是共用的,你就已经在一条会出问题的链路上了。另外只要站点将来有可能加德语,ß那一条就迟早会碰到,而那时候slug已经生成完了,改起来比现在贵得多。
怎么在半小时内判断自己的站有没有中招?
最快的办法是往返测试。挑二十个目标语言的常用词,每个词做一次转大写再转回小写,跟原串逐字节比对。回不到原串的词就是命中了有损转换。这一步不需要懂那门语言,也不需要母语者参与,写个十行的脚本就能跑完。如果全部往返成功,再去检查站内搜索的索引侧和查询侧用的是不是同一套规则,这两处覆盖了绝大多数真实故障。
已经生成的那批错误slug,到底要不要改?
先看这批地址的现状再决定。已经有外链、有收录、有稳定流量的,改动的代价通常大于收益,正确做法是保留地址、修正规则、只让新内容走新规则。完全没有流量也没有外链的,直接改并配上跳转,成本很低。真正必须改的是那些错到用户一眼能看出来的地址,比如词形已经变成另一个词或者干脆不成词的,那种地址挂在导航上会持续损害信任。
数据库排序规则到底该选哪一个?
没有一个万能选项,但有一条排除法:别为了省事在全库上挂一个不区分大小写、不区分重音的规则。那种配置会把好几类本来不同的词判成相等,唯一索引会莫名其妙地冲突,去重结果也不可信。比较稳的做法是库级用一个中性的规则,需要按语言排序的那几个字段单独指定这门语言的规则,同时把展示用的原形单独存一列,别让排序规则决定你能拿回什么数据。
站内搜索应该用折叠规则还是转小写?
索引和查询两侧都用折叠规则,展示层用原形。折叠是专门为比较设计的,它会把位置变体、大小写差异统一到同一个键上,而且结果稳定,不受运行环境的地区设置影响。转小写是给人看的,它要照顾正字法,反而不适合当比较键。两侧规则必须一致这一点比选哪一套更重要,一致但不完美的配置,也比两侧各用一套的配置好用得多。
母语审校能不能发现这类问题?
能发现一部分,但要看你给的验收范围。母语者读正文段落时对怪词非常敏感,一眼就能挑出来;可导航标签、按钮文案、面包屑、筛选项这些位置往往不在审校清单里,而它们恰恰是全大写和首字母大写最集中的地方。解决办法是在清单里单独列一行,让审校者把这几类短文本逐个扫一遍,额外成本几乎为零。
这跟去掉变音符号是不是一回事?
不是,两者的性质完全不同。去变音是一个你主动选择的覆盖策略,目的是接住那些打不出特殊字母的用户,取舍是明摆着的。大小写转换没人当成策略,它被当成格式化,所以从来没有人评估过它的取舍。真正危险的是把这两个动作写进同一个函数,那样你就再也没法单独关掉其中一个,也没法回答某个词到底是被哪一步改坏的。
权威参考资料
本文标题:《转成小写这一步在英文站上从来不出事,到了土耳其语站它把词改成了另一个词》
本文链接:https://zhangwenbao.com/minor-language-case-folding-lowercase-turkish-dotless-i.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0