For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-intl-segmenter.md.
  • 简体中文
  • Intl.Segmenter: Unicode Segmentation in JavaScript S4

    中文标题:Intl.Segmenter:JavaScript 中的 Unicode 分段

    提案概览
    提案速览

    该提案引入了 Intl.Segmenter API 用于 Unicode 文本分段,解决了根据 locale 特定规则将字符串拆分为字素、单词或句子的需求。它提供了可迭代和随机访问的接口来获取带有元数据的分段。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    Intl.Segmenter:JavaScript 中的 Unicode 分段

    Stage 4 提案,冠军 Richard Gibson

    动机

    一个码点不是“字母”或屏幕上的显示单元。这个名称属于字素(grapheme),它可能由多个码点组成(例如,包括重音符号、韩文组合字符)。Unicode 定义了一个字素分段算法来找到字素之间的边界。这可能对实现高级编辑器/输入法或其他形式的文本处理很有用。

    Unicode 还定义了一个算法来找到单词和句子之间的边界,CLDR 针对每个 locale 进行定制。这些边界可能很有用,例如,在实现一个文本编辑器时,该编辑器具有跳转或高亮单词和句子的命令。

    字素、单词和句子分段在 UAX 29 中定义。Web 浏览器需要实现这种分段才能正常工作,将其提供给 JavaScript 可以节省内存和网络带宽,而不需要开发者自己在 JavaScript 中实现它。

    Chrome 已经发布了一个非标准的分段 API 称为 Intl.v8BreakIterator 几年了。然而,由于一些原因,这个 API 似乎不适合标准化。本解释器概述了一个新的 API,试图更符合现代、ES2015 之后的 JavaScript API 设计。

    示例

    分段迭代

    Intl.Segmenter 实例的 segment 方法返回的对象找到边界,并通过 Iterable 接口 公开它们之间的分段。

    // 创建一个 locale 特定的单词分段器
    let segmenter = new Intl.Segmenter("fr", {granularity: "word"});
    
    // 使用它来获取字符串的迭代器
    let input = "Moi?  N'est-ce pas.";
    let segments = segmenter.segment(input);
    
    // 使用它进行分段!
    for (let {segment, index, isWordLike} of segments) {
      console.log("segment at code units [%d, %d): «%s»%s",
        index, index + segment.length,
        segment,
        isWordLike ? " (word-like)" : ""
      );
    }
    // console.log 输出:
    // segment at code units [0, 3): «Moi» (word-like)
    // segment at code units [3, 4): «?»
    // segment at code units [4, 6): «  »
    // segment at code units [6, 11): «N'est» (word-like)
    // segment at code units [11, 12): «-»
    // segment at code units [12, 14): «ce» (word-like)
    // segment at code units [14, 15): « »
    // segment at code units [15, 18): «pas» (word-like)
    // segment at code units [18, 19): «.»

    为了灵活性和高级用例,它们还支持直接随机访问。

    // ┃0 1 2 3 4 5┃6┃7┃8┃9
    // ┃A l l o n s┃-┃y┃!┃
    let input = "Allons-y!";
    
    let segmenter = new Intl.Segmenter("fr", {granularity: "word"});
    let segments = segmenter.segment(input);
    let current = undefined;
    
    current = segments.containing(0)
    // → { index: 0, segment: "Allons", isWordLike: true }
    
    current = segments.containing(5)
    // → { index: 0, segment: "Allons", isWordLike: true }
    
    current = segments.containing(6)
    // → { index: 6, segment: "-", isWordLike: false }
    
    current = segments.containing(current.index + current.segment.length)
    // → { index: 7, segment: "y", isWordLike: true }
    
    current = segments.containing(current.index + current.segment.length)
    // → { index: 8, segment: "!", isWordLike: false }
    
    current = segments.containing(current.index + current.segment.length)
    // → undefined

    API

    polyfill 用于此提案的历史快照

    new Intl.Segmenter(locale, options)

    创建一个新的 locale 相关的分段器。 如果提供了 options,它被视为一个对象,其 granularity 属性指定分段器的粒度("grapheme"、"word" 或 "sentence",默认为 "grapheme")。

    Intl.Segmenter.prototype.segment(string)

    使用分段器的 locale 和粒度,为输入字符串创建一个新的 Iterable %Segments% 实例。

    分段数据

    分段由具有以下数据属性的普通对象描述:

    • segment 是字符串分段。
    • index 是分段在字符串中开始的码元索引。
    • input 是被分段的字符串。
    • isWordLike 当粒度为 "word" 且分段是 word-like(由字母/数字/表意文字等组成)时为 true,当粒度为 "word" 且分段不是 word-like(由空格/标点等组成)时为 false,当粒度不是 "word" 时为 undefined

    %Segments%.prototype 的方法:

    %Segments%.prototype.containing(index)

    返回一个分段数据对象,描述字符串中包含指定索引处码元的分段,如果索引超出范围则返回 undefined

    %Segments%.prototype[Symbol.iterator]

    创建一个新的 %SegmentIterator% 实例,该实例将使用分段器的 locale 和粒度惰性地在输入字符串中查找分段,并跟踪其在字符串中的当前位置。

    %SegmentIterator%.prototype 的方法:

    %SegmentIterator%.prototype.next()

    next 方法实现了 Iterator 接口,查找下一个分段并返回相应的 IteratorResult 对象,其 value 属性是如上所述的分段数据对象。

    常见问题解答

    为什么我们要为字素边界传递 locale 和选项包?难道只有一种方法吗?

    情况有点复杂,例如,对于印度文字。正在努力更好地支持这些文字的字素边界选项;参见 这个 bug,特别是 这个 CLDR wiki 页面。似乎 CLDR/ICU 还不支持这个,但计划中。

    我们不应该把新的 API 放到内置模块中吗?

    如果内置模块在进入 Stage 3 之前出现,那似乎是个好选择。然而,到目前为止,TC39 的想法是不要让任何一方阻塞另一方。内置模块仍有一些大问题需要解决,例如,polyfills 如何/是否应该与其交互。

    为什么不包括换行(line breaking)?

    在此 API 的早期版本中提供了换行,但将其排除在外,因为仅仅一个换行 API 是不完整的:换行通常用于文本布局,而文本布局需要更大的一套 API,例如,确定渲染文本字符串的宽度。因此,我们建议继续开发一个换行 API,作为 CSS Houdini 工作的一部分。

    为什么不包括连字符(hyphenation)?

    由于各种原因,连字符预计会有不同形式的 API 形状:

    • 添加连字符断点可能会改变受影响文本的拼写
    • 可能存在不同优先级的连字符断点
    • 连字符在行布局和字体渲染中以更复杂的方式发挥作用,我们可能希望在该级别公开它(例如,在 Web 平台而不是 ECMAScript 中)
    • 连字符在国际化世界中只是一个不太成熟的东西。CLDR 和 ICU 还不支持它;某些 Web 浏览器现在才在 CSS 中获得它的支持。它通常做得并不完美。它可能需要更多时间成熟。相比之下,单词、字素、句子和换行在 Unicode 规范中已经存在了很长时间;这是一个万事俱备的项目。

    为什么随机访问是无状态的?

    有可能在 %SegmentIterator%.prototype 上公开修改内部状态的方法(例如,seek([inclusiveStartIndex = thisIterator.index + 1])seekBefore([exclusiveLastIndex = thisIterator.index])),事实上这些是早期设计的一部分。 它们被丢弃是为了与其他 ECMA-262 迭代器的一致性(其移动总是向前且没有间隙)。 如果实际使用表明它们的缺失是一个人体工程学和/或性能缺陷,它们可以在后续提案中添加。

    为什么这是一个 Intl API 而不是 String 方法?

    所有这些边界类型实际上都是 locale 相关的,有些允许复杂的选项。segment 方法的结果是一个 SegmentIterator。对于许多像这样的非平凡情况,类似的 API 被放在 ECMA-402 的 Intl 对象中。这允许在每次实例化时完成的工作共享,从而提高性能。我们可以作为后续提案在 String 上添加一个便捷方法。

    索引具体指什么?

    索引 n 指字符串中可能是分段开始的码元索引。 例如,当按英语单词迭代字符串 "Hello, world💙" 时,分段将从索引 0、5、6、7 和 12 开始(即,字符串被分段为 ┃Hello┃,┃ ┃world┃💙┃,最后一个分段由两个码元的代理对组成,编码一个码点)。 这些边界索引的定义不依赖于使用向前还是向后迭代。

    分段空字符串时会发生什么?

    不会找到任何分段,迭代器将在第一次 next() 访问时立即完成。

    当我尝试使用非数字值进行随机访问时会发生什么?

    某个人 在 QA。😉 containing 参数被处理为整数 Number——nullundefinedNaN 变为 0,布尔值变为 0 或 1,字符串被解析为字符串数字字面量,对象被转换为原始值,Symbol 和 BigInt 会抛出 TypeError 异常。小数部分被截断,但无限数字按原样接受(尽管它们总是超出范围,因此永远不会找到分段)。

    实现