我在现有答案中没有看到与星体平面代码点或国际化有关的任何问题。使用给定脚本,“大写”在每种语言中并不意味着同一件事。
最初,我看不到任何与星体平面代码点相关的问题的答案。有一个,但是有点埋没了(我想是这样的!)
建议的大多数功能如下:
function capitalizeFirstLetter(str) {
return str[0].toUpperCase() + str.slice(1);
}
但是,某些大小写的字符不在BMP(基本多语言平面,代码点U + 0至U + FFFF)之外。例如,使用以下Deseret文本:
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉"); // "𐐶𐐲𐑌𐐼𐐲𐑉"
这里的第一个字符不能大写,因为字符串的数组索引属性无法访问“字符”或代码点*。他们访问UTF-16代码单元。切片时也是如此—索引值指向代码单位。
碰巧UTF-16代码单位为1:1,USV代码点在两个范围内(包括U + 0至U + D7FF和U + E000至U + FFFF)。大多数情况下的字符都属于这两个范围,但不是全部。
从ES2015开始,处理此问题变得更加容易。String.prototype[@@iterator]
产生对应于代码点**的字符串。因此,例如,我们可以这样做:
function capitalizeFirstLetter([ first, ...rest ]) {
return [ first.toUpperCase(), ...rest ].join('');
}
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉") // "𐐎𐐲𐑌𐐼𐐲𐑉"
对于更长的字符串,这可能效率不高***-我们真的不需要迭代其余部分。我们可以String.prototype.codePointAt
用来获取第一个(可能的)字母,但是我们仍然需要确定切片应从何处开始。避免迭代其余部分的一种方法是测试第一个代码点是否在BMP之外。如果不是,则切片从1开始,如果是,则切片从2开始。
function capitalizeFirstLetter(str) {
const firstCP = str.codePointAt(0);
const index = firstCP > 0xFFFF ? 2 : 1;
return String.fromCodePoint(firstCP).toUpperCase() + str.slice(index);
}
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉") // "𐐎𐐲𐑌𐐼𐐲𐑉"
您可以使用按位数学,而不是在> 0xFFFF
那里,但是用这种方式可能更容易理解,并且两者都可以实现相同的目的。
我们还可以在ES5及以下版本中通过在必要时进一步扩展该逻辑来使这项工作生效。ES5中没有用于处理代码点的内在方法,因此我们必须手动测试第一个代码单元是否为代理****:
function capitalizeFirstLetter(str) {
var firstCodeUnit = str[0];
if (firstCodeUnit < '\uD800' || firstCodeUnit > '\uDFFF') {
return str[0].toUpperCase() + str.slice(1);
}
return str.slice(0, 2).toUpperCase() + str.slice(2);
}
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉") // "𐐎𐐲𐑌𐐼𐐲𐑉"
在开始时,我还提到了国际化注意事项。其中有些是很难解释,因为他们不仅需要知识是什么正在使用的语言,但也可能需要在语言中的单词的具体知识。例如,爱尔兰语字母“ mb”在单词开头大写为“ mB”。另一个示例,德语eszett,从不以单词开头(afaik),但仍有助于说明问题。小写的eszett(“ß”)大写为“ SS”,但是“ SS”可以小写为“ß”或“ ss”-您需要带德语的带外知识才能知道哪种正确!
这类问题最著名的例子就是土耳其语。在土耳其语拉丁语中,i的大写形式为İ,而I的小写形式为ı,它们是两个不同的字母。幸运的是,我们确实可以解决这个问题:
function capitalizeFirstLetter([ first, ...rest ], locale) {
return [ first.toLocaleUpperCase(locale), ...rest ].join('');
}
capitalizeFirstLetter("italy", "en") // "Italy"
capitalizeFirstLetter("italya", "tr") // "İtalya"
在浏览器中,用户最喜欢的语言标记由表示navigator.language
,在处找到了按优先顺序排列的列表navigator.languages
,并且可以(通常)Object(element.closest('[lang]')).lang || YOUR_DEFAULT_HERE
在多语言文档中获得给定DOM元素的语言。
在ES2018中引入的在RegExp中支持Unicode属性字符类的代理中,我们可以通过直接表达我们感兴趣的字符来进一步清理内容:
function capitalizeFirstLetter(str, locale=navigator.language) {
return str.replace(/^\p{CWU}/u, char => char.toLocaleUpperCase(locale));
}
可以进行一些微调,以同样准确地处理字符串中多个单词的大写。该CWU
或Changes_When_Uppercased字符属性匹配其中,那么,改变大写当所有代码点。我们可以尝试了这一点与titlecased有向图的人物,如荷兰ij例如:
capitalizeFirstLetter('ijsselmeer'); // "IJsselmeer"
在撰写本文时(2020年2月),Firefox / Spidermonkey尚未实现过去两年中引入的任何RegExp功能*****。您可以在Kangax兼容表中查看此功能的当前状态。Babel能够使用对等效模式的属性引用来编译RegExp常量,而无需使用等效模式,但是请注意,生成的代码可能非常庞大。
提出这个问题的人极有可能与Deseret的大写字母或国际化无关。但是,意识到这些问题是一件好事,因为即使目前不关心这些问题,您也很有可能最终会遇到它们。它们不是“边缘”情况,或更确切地说,它们不是按边缘定义的情况—在整个国家,大多数人还是会说土耳其语,将代码单元与代码点合并是一个相当常见的错误来源(尤其是与关于表情符号)。字符串和语言都非常复杂!
*从某种意义上说,UTF-16 / UCS2的代码单元也是Unicode代码点,例如从技术上说U + D800是一个代码点,但这并不是它的“含义”……有点……虽然很漂亮模糊。但是,替代品绝对不是USV(Unicode标量值)。
**尽管如果代理代码单元是“孤立的”(即,不是逻辑对的一部分),您仍然可以在这里获得代理。
*** 也许。我还没有测试。除非您确定大写是一个有意义的瓶颈,否则我可能不会大惊小怪-选择您认为最清晰易读的内容。
****这种功能可能希望同时测试第一个和第二个代码单元,而不仅仅是第一个,因为第一个单元可能是孤立的代理。例如,输入“ \ uD800x”将按原样大写X,这可能会或可能不会出现。
***** 如果您想更直接地跟踪进度,这是Bugzilla问题。