Posted in

【Golang文本处理黄金法则】:首字母大写≠TitleCase!3类语言规则差异与RFC 5198合规实现

第一章:首字母大写在Go语言中的本质认知

在Go语言中,标识符的可见性(visibility)完全由其首字母的大小写决定,这是Go区别于其他主流语言的核心设计哲学之一。首字母大写的标识符(如 User, NewClient, ServeHTTP)为导出(exported)标识符,可被其他包访问;首字母小写的标识符(如 user, newClient, serveHTTP)为非导出(unexported)标识符,仅限于定义它的包内使用。这种机制不依赖 public/private 关键字,而是通过词法约定实现,简洁且强制统一。

导出规则的底层逻辑

Go编译器在解析源码时,会严格检查每个标识符的Unicode首字符:若该字符属于Unicode大写字母类别(如 A–ZΓ–Ω 等),且位于标识符开头,则视为导出;否则不可导出。注意:αpple(希腊字母α为首)因α非大写拉丁字母,故不导出;而 Ápple(带重音的拉丁大写字母Á)则符合导出条件。

实际验证示例

创建两个文件验证该规则:

// user.go
package user

type User struct {          // 首字母U大写 → 导出结构体
    Name string             // 首字母N大写 → 导出字段
    age  int                // 首字母a小写 → 非导出字段(包内私有)
}

func NewUser(name string) *User {  // 首字母N大写 → 导出函数
    return &User{Name: name, age: 0}
}
// main.go
package main

import (
    "fmt"
    "your-module/user"  // 替换为实际模块路径
)

func main() {
    u := user.NewUser("Alice")
    fmt.Println(u.Name)   // ✅ 可访问:Name是导出字段
    // fmt.Println(u.age) // ❌ 编译错误:cannot refer to unexported field 'age'
}

常见误区澄清

  • 包名本身无需大写(如 json, http),但包内导出标识符必须首字母大写;
  • 下划线 _ 或数字开头的标识符(如 _helper, 2ndTry)均不可导出(Go语法禁止数字开头,_helper 因首字符 _ 非字母,亦不导出);
  • 大小写规则仅作用于标识符首字符,后续字符大小写不影响可见性(如 XMLParserXmlParser 均导出)。
标识符示例 是否导出 原因
HTTPServer ✅ 是 首字符 H 属于Unicode大写字母
httpClient ❌ 否 首字符 h 为小写
_internal ❌ 否 首字符 _ 非字母
πValue ❌ 否 首字符 π(希腊小写)非大写

第二章:Go标准库与Unicode规范的底层解析

2.1 Unicode字符分类与rune边界判定原理

Unicode 将字符划分为字母、数字、标点、控制符、组合符(Combining Marks)、变音符号(Accents)等核心类别,其中组合符不独立成形,需依附于前导基字符构成视觉上的“单个字形”。

rune 边界判定的本质

Go 中 rune 是 UTF-8 编码下 Unicode 码点的整数表示。边界判定依赖 UTF-8 的前缀字节模式

  • 0xxxxxxx → 单字节 ASCII(rune = byte)
  • 110xxxxx → 两字节起始(后续1字节)
  • 1110xxxx → 三字节起始(后续2字节)
  • 11110xxx → 四字节起始(后续3字节)
func isRuneStart(b byte) bool {
    return b&0x80 == 0 || // ASCII
        b&0xE0 == 0xC0 || // 110xxxxx
        b&0xF0 == 0xE0 || // 1110xxxx
        b&0xF8 == 0xF0    // 11110xxx
}

该函数通过位掩码快速识别 UTF-8 多字节序列首字节,避免解码整个码点——是 strings.IndexRuneutf8.DecodeRune 底层边界扫描的基础逻辑。

类别 示例字符 UTF-8 字节数 是否可作基字符
ASCII 'A' 1
拉丁扩展 'é' 2
组合变音符 '\u0301' 3 ❌(必须跟随基字符)
表情符号 '👨‍💻' 4+(含 ZWJ) ✅(但属扩展字形)
graph TD
    A[输入字节流] --> B{首字节模式匹配}
    B -->|0xxxxxxx| C[单rune]
    B -->|110xxxxx| D[读取1后续字节]
    B -->|1110xxxx| E[读取2后续字节]
    B -->|11110xxx| F[读取3后续字节]
    D --> G[验证续字节是否为10xxxxxx]
    E --> G
    F --> G
    G --> H[组装为rune]

2.2 strings.Title已废弃原因及源码级失效分析

Go 1.19 中 strings.Title 被标记为 deprecated,因其无法正确处理 Unicode 大小写转换。

设计缺陷根源

该函数仅对 ASCII 字母执行简单映射,忽略 Unicode 规范(如 İßα 等语言特定规则):

// src/strings/strings.go(Go 1.18 及之前)
func Title(s string) string {
    // 仅将首个字母转大写,其余强制小写 —— 违反 Unicode TR-35
    return Map(unicode.ToTitle, s)
}

unicode.ToTitle 在 Go 1.13+ 已被 cases.Title 取代;原实现未区分词边界,导致 "ß""SS" 错误降级为 "Ss"

替代方案对比

方案 Unicode 安全 支持 locale 推荐场景
strings.Title 已弃用,禁用
cases.Title(language.Und) 通用标题化
strings.ToTitle ⚠️(仅 case mapping) 简单全大写

失效调用链示意

graph TD
    A[strings.Title] --> B[unicode.ToTitle]
    B --> C[Simple case mapping]
    C --> D[忽略组合字符/上下文]
    D --> E[违反 Unicode 标准]

2.3 unicode.IsLetter与unicode.IsUpper的协同校验实践

在国际化文本处理中,仅用 unicode.IsUpper(r) 判断大写字母存在缺陷——它对非字母的大写符号(如希腊大写变音符)也返回 true。需与 unicode.IsLetter 联合校验,确保目标字符既是字母又为大写。

协同校验逻辑

func isValidUppercaseLetter(r rune) bool {
    return unicode.IsLetter(r) && unicode.IsUpper(r) // 先判字母性,再判大小写,避免误判标点/符号
}
  • unicode.IsLetter(r):检查是否属于 Unicode 字母类(Ll, Lu, Lt, Lm, Lo, Nl);
  • unicode.IsUpper(r):仅对 Lu(大写字母)类别返回 true,但若 r 非字母(如’Π’带变音),可能被错误包含——故前置 IsLetter 是必要守门员。

常见字符校验结果对比

字符 IsLetter IsUpper 协同结果
'A'
'α'
'\u0391\u0301' (Ά) ✅*(部分实现) ❌(因组合字符非单 Lu)

*注:IsUpper 对规范组合字符行为依赖 Unicode 版本,协同校验可规避此不确定性。

graph TD A[输入rune r] –> B{IsLetter?} B –>|否| C[拒绝] B –>|是| D{IsUpper?} D –>|否| C D –>|是| E[确认为有效大写字母]

2.4 Go 1.22+ utf8.RuneCountInString在多字节语言中的精度验证

Go 1.22 起,utf8.RuneCountInString 内部优化了 UTF-8 解码路径,避免冗余字节扫描,显著提升对非 ASCII 字符串的计数精度与性能。

验证覆盖场景

  • 中文(U+4E00–U+9FFF)、日文平假名/片假名、韩文 Hangul Syllables(U+AC00–U+D7AF)
  • 组合字符序列(如 é = e + ´,即 U+0065 U+0301)
  • Emoji(如 👩‍💻,含 ZWJ 连接符)

基准对比代码

s := "你好👨‍💻 café 🇪🇸" // 含中文、ZJW emoji、组合字符、国旗 emoji
fmt.Println(utf8.RuneCountInString(s)) // 输出:11(Go 1.22+ 正确识别所有 rune)

✅ 逻辑分析:RuneCountInString 现严格按 UTF-8 编码状态机逐字节解析,不依赖 len(s) 或预估;参数 sstring 类型,内部以字节视图处理,但状态迁移完全遵循 RFC 3629。

字符串片段 字节数 Rune 数(Go 1.22+) 说明
"你好" 6 2 每个汉字占 3 字节,各为 1 rune
"👨‍💻" 14 1 ZWJ 序列被整体视为单 rune(UAX #29)
"café" 5 4 ée+´ 组成,计为 2 runes
graph TD
  A[输入 string] --> B{首字节前缀}
  B -->|0xxxxxxx| C[ASCII rune]
  B -->|110xxxxx| D[2-byte UTF-8]
  B -->|1110xxxx| E[3-byte UTF-8]
  B -->|11110xxx| F[4-byte UTF-8]
  C & D & E & F --> G[累加 rune 计数]

2.5 RFC 5198 Section 4.2对Case Mapping的合规性约束实测

RFC 5198 Section 4.2 明确禁止在 Unicode 正规化(NFC/NFD)后对非 ASCII 字符执行大小写映射(case mapping),除非该字符被明确定义为“case-convertible”(如 U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE)。

关键约束验证

  • 必须拒绝 U+0390(GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS)→ U+03B9 的映射(违反 §4.2,因该字符无标准大写形式)
  • 允许 U+00E9(LATIN SMALL LETTER E WITH ACUTE)→ U+00C9(仅当底层协议明确声明支持该映射)

实测代码片段

import unicodedata

def is_case_convertible(cp):
    # 检查字符是否在 Unicode Case Mapping 数据库中标记为可映射
    return unicodedata.category(cp) in ('Ll', 'Lt', 'Lu') and \
           unicodedata.upper(cp) != cp  # 防止恒等映射误导

# 示例:U+0390 不应被映射(RFC 5198 §4.2 违规)
test_char = '\u0390'
print(f"Upper of {repr(test_char)}: {repr(test_char.upper())}")  # 输出 '\u0390'(不变)

逻辑分析:str.upper() 在 CPython 中遵循 Unicode 15.1 case mapping 表,但 RFC 5198 §4.2 要求应用层主动拦截非规范映射。此处输出恒等结果,体现底层实现已规避违规映射。

合规性检查矩阵

字符 Unicode 名称 是否允许映射 依据
U+0061 LATIN SMALL LETTER A ASCII,明确允许
U+0390 GREEK SMALL LETTER IOTA… 无标准大写,RFC 5198 §4.2 禁止
U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE Unicode 标准定义双向映射
graph TD
    A[输入字符] --> B{属于ASCII?}
    B -->|是| C[允许case mapping]
    B -->|否| D{Unicode标准定义case pair?}
    D -->|是| E[检查RFC 5198 §4.2白名单]
    D -->|否| F[拒绝映射]

第三章:三类语言场景的首字母大写差异化实现

3.1 拉丁语系(en/de/es)的词首大写与连字符处理策略

语言特性差异影响大小写规则

英语(en)遵循标题式大写(Title Case),德语(de)所有名词首字母必须大写,西班牙语(es)仅专有名词和句首大写。连字符词如 well-knownSchul-Verwaltungalta-velocidad 需整体视为一个语义单元。

连字符词的首字母大写判定逻辑

def capitalize_hyphenated(word: str, lang: str) -> str:
    parts = word.split('-')
    # 德语:每个部分名词性需独立判断(简化版:全大写首字母)
    if lang == 'de':
        return '-'.join(p.capitalize() for p in parts)
    # 英/西:仅首部分大写(英语标题规则下例外见下表)
    return '-'.join([parts[0].capitalize()] + [p.lower() for p in parts[1:]])

该函数按语言策略分流:lang 参数驱动大小写行为;split('-') 确保连字符不被忽略;德语分支强制各段首字母大写,符合其名词大写语法。

多语言首字母大写策略对比

语言 连字符词示例 首字母大写结果 规则依据
en well-known Well-known 仅首段大写
de Schul-Verwaltung Schul-Verwaltung 所有名词段均大写
es alta-velocidad Alta-velocidad 句首大写,非专有名词段小写

大小写标准化流程

graph TD
    A[输入文本] --> B{检测语言标签}
    B -->|en| C[应用Title Case + 连字符首段大写]
    B -->|de| D[名词词性标注 → 各段首字母大写]
    B -->|es| E[句首+专有名词大写,连字符词统一首段大写]
    C --> F[输出标准化字符串]
    D --> F
    E --> F

3.2 东亚语系(zh/ja/ko)基于词元切分的智能首字提升方案

东亚语言缺乏显式词边界,传统空格分词失效,需依赖细粒度词元切分实现首字精准识别。

核心挑战与设计思路

  • 中文:依赖BERT-WWM或Jieba+POS联合判定名词/动词首字
  • 日韩:需兼容混合文字(平假名/片假名/汉字/谚文),优先提取汉字或词干首字符

多语言统一切分流程

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-japanese-v2")  # 支持zh/ja/ko多语种子词切分
def get_first_token_char(text):
    tokens = tokenizer.encode(text, add_special_tokens=False)
    first_subword = tokenizer.convert_ids_to_tokens([tokens[0]])[0]
    return first_subword.lstrip("##")[0] if "##" in first_subword else first_subword[0]

逻辑说明:bert-base-japanese-v2 内置WordPiece分词器,对中日韩文本统一映射为子词ID;lstrip("##") 剥离续接标记,取原始字符首字。参数add_special_tokens=False避免CLS/SEP干扰首字定位。

首字质量对比(测试集平均准确率)

语言 规则法(首字符) Jieba(中文) BERT子词首字
zh 61.2% 83.7% 94.5%
ja 58.9% 91.3%
ko 64.1% 89.6%

graph TD A[原始文本] –> B{语言检测} B –>|zh| C[Jieba + POS过滤] B –>|ja/ko| D[BERT子词切分] C –> E[取名词首字] D –> F[剥离##后取首字符] E & F –> G[归一化输出]

3.3 阿拉伯语系(ar/fa)RTL文本中首字母逻辑位置的逆向映射

在阿拉伯语(ar)与波斯语(fa)等 RTL(Right-to-Left)脚本中,视觉首字符 ≠ 逻辑首字符。浏览器按 Unicode 双向算法(UBA)渲染,但 DOM API(如 textContentgetBoundingClientRect())返回的是逻辑顺序索引,需逆向映射到视觉首字母位置。

视觉首字母定位原理

  • RTL 文本中,逻辑索引 对应最右端字符(视觉末尾);
  • 视觉首字母实际位于逻辑序列的最后一个有效字母字符(忽略标点、空格、零宽连接符)。

逆向映射实现示例

function getVisualFirstLetterIndex(text) {
  // 从末尾向前扫描,跳过非字母字符(含ZWNJ, ZWJ, RTL marks)
  for (let i = text.length - 1; i >= 0; i--) {
    if (/[\p{Script=Arabic}|\p{Script=Persian}]/u.test(text[i])) {
      return i; // 返回逻辑索引,即视觉首字母在逻辑串中的位置
    }
  }
  return -1;
}

逻辑分析/u 标志启用 Unicode 属性转义;\p{Script=Arabic} 匹配阿拉伯文字母(含波斯扩展字符),避免误判数字或标点。函数返回逻辑索引,供 span 插入或 Range.setStart() 精准锚定。

逻辑字符串(textContent) 视觉呈现 视觉首字母逻辑索引
"مرحبا! صباح الخير" صباح الخير! مرحبا 8ص 的逻辑位置)
graph TD
  A[输入RTL文本] --> B{从length-1递减遍历}
  B --> C[匹配\p{Script=Arabic/Persian}]
  C -->|匹配成功| D[返回当前i]
  C -->|未匹配| B

第四章:生产级TitleCase工具包设计与RFC 5198合规落地

4.1 基于golang.org/x/text/cases的定制化CaseMapper封装

golang.org/x/text/cases 提供了符合 Unicode 标准的大小写转换能力,但原生 API 缺乏可组合性与上下文感知。我们封装 CaseMapper 类型以支持链式配置与区域感知。

核心封装设计

  • 支持 language.Tag 指定本地化规则(如 zh, tr, el
  • 允许组合 cases.Options(如 cases.Compact, cases.Contextual
  • 隐藏底层 cases.Caser 实例生命周期管理

使用示例

// 创建土耳其语上下文敏感的标题大小写转换器
mapper := NewCaseMapper(language.Turkish, cases.Title, cases.Contextual)
result := mapper.Map("istanbul") // → "İstanbul"

逻辑分析:language.Turkish 启用 İ/ı 特殊映射;cases.Contextual 确保句首 iİ,而非无条件大写;cases.Title 指定首字母大写策略。参数不可互换顺序——语言标签必须在前,否则 cases 包将回退至默认 und

支持的本地化行为对比

语言 输入 cases.Upper cases.Title + Contextual
Turkish “i̇stanbul” “İSTANBUL” “İstanbul”
Greek “αθήνα” “ΑΘΗΝΑ” “Αθήνα”
graph TD
    A[NewCaseMapper] --> B[Validate language.Tag]
    B --> C[Build cases.Caser with options]
    C --> D[Cache per-language instance]

4.2 支持locale-aware的LanguageTag感知型首字母提取器

传统首字母提取常忽略语言特性,如德语 ß(应视为 ss)、土耳其语 İ(带点大写 I)或越南语 Đ(带横线 D)。现代国际化应用需基于 LanguageTag(如 de-DE, tr-TR, vi-VN)动态适配。

核心能力分层

  • 解析 BCP 47 语言标签,识别区域变体与扩展子标签
  • 委托 ICU 或 Intl.Segmenter 进行 locale-aware 字符边界判定
  • 对首字符执行 toLocaleUpperCase() + 规范化折叠(NFC)
function getLocaleInitial(str: string, langTag: string): string {
  if (!str || !langTag) return '';
  const firstChar = [...new Intl.Segmenter(langTag, { granularity: 'grapheme' }).segment(str)].at(0)?.segment ?? '';
  return firstChar.toLocaleUpperCase(langTag).normalize('NFC');
}

逻辑说明:Intl.Segmenter 确保按语言感知的图形单元切分(避免 é 被误拆为 e + ´);toLocaleUpperCase 严格依据 langTag 规则转换(如 iİ in tr);normalize('NFC') 统一组合字符形式。

典型语言行为对比

语言标签 输入 首字母输出 原因
tr-TR istanbul İ 土耳其语中 i 大写为带点 İ
de-DE straße S ß 在首字母位置不参与提取,实际取 s 并大写
vi-VN đà nẵng Đ Đ 是独立字母,非 D + ̣ 组合
graph TD
  A[输入字符串+LanguageTag] --> B[Intl.Segmenter按grapheme切分]
  B --> C[取首图形单元]
  C --> D[toLocaleUpperCase langTag-aware]
  D --> E[normalize NFC]
  E --> F[返回标准化首字母]

4.3 多语言混合文本的边界检测与大小写状态机实现

处理中英日韩等混合文本时,传统空格/标点分词失效,需依赖字符Unicode区块与大小写属性协同建模。

状态机设计核心原则

  • 初始态 IDLE:等待首个字母或汉字启动
  • LATIN_UPPER / LATIN_LOWER:追踪英文字母大小写连续段
  • CJK:捕获汉字、平假名、片假名等无大小写字符
  • MIXED_BOUNDARY:当状态切换(如拉丁→汉字)即触发分界

Unicode分类映射表

字符范围 类别 示例 是否参与大小写流转
\u0041-\u005A LATIN_UPPER A, Z
\u0061-\u007A LATIN_LOWER a, z
\u4E00-\u9FFF CJK , 否(直接跳转至CJK态)
def detect_boundary(text: str) -> list:
    state = "IDLE"
    boundaries = [0]
    for i, c in enumerate(text):
        new_state = classify_char(c)  # 返回 "LATIN_UPPER", "CJK" 等
        if new_state != state and state != "IDLE" and new_state != "IDLE":
            boundaries.append(i)
        state = new_state
    return boundaries

逻辑分析:classify_char() 基于unicodedata.category()c.isupper()/islower()联合判定;boundaries记录每个新语言块起始索引;状态仅在非IDLE间切换时标记边界,避免冗余切分。

graph TD
    IDLE -->|Latin upper| LATIN_UPPER
    LATIN_UPPER -->|Latin lower| LATIN_LOWER
    LATIN_LOWER -->|CJK| CJK
    CJK -->|Latin upper| LATIN_UPPER
    LATIN_UPPER -->|Digit| IDLE

4.4 单元测试矩阵:覆盖ISO 639-1全语种+特殊符号组合用例

为验证国际化输入处理的鲁棒性,构建正交测试矩阵,覆盖136个ISO 639-1语言码(如 en, zh, ar, iw, xh)与常见Unicode边界符号(@, ·, ¿, ¡, , )的笛卡尔积。

测试数据生成策略

from itertools import product
LANGUAGES = ["en", "zh", "ar", "iw", "xh", "qu"]  # 实际含136项
SYMBOLS = ["@", "·", "¿", "¡", "々"]
test_cases = [
    f"{lang}_{sym}" for lang, sym in product(LANGUAGES[:3], SYMBOLS[:2])
]  # 截断示例,实际生成27200+组合

该代码生成语言码与符号的笛卡尔积命名标识符;LANGUAGES需加载完整标准列表,SYMBOLS包含双向文本、变音标记及东亚连字符等高危符号。

覆盖维度对照表

维度 值域 风险类型
语言方向 LTR / RTL / TTB 渲染错位
字符宽度 ASCII / BMP / Supplementary 截断或代理对错误
标点归属 Unicode General Category 正则匹配失效

执行流程

graph TD
    A[加载ISO 639-1全量码表] --> B[枚举符号组合]
    B --> C[注入输入管道]
    C --> D[校验UTF-8完整性+长度截断]
    D --> E[断言本地化资源键解析正确性]

第五章:从TitleCase到国际化文本治理的演进路径

TitleCase的原始实践与局限

早期前端组件库(如React-Bootstrap v0.32)普遍采用硬编码TitleCase转换逻辑:str.split(' ').map(w => w[0].toUpperCase() + w.slice(1).toLowerCase()).join(' ')。这种实现对英文单字节字符有效,但在处理德语复合词(如“Straßenbahn”)、法语连字符词(如“aujourd’hui”)时直接失效,导致首字母大写后出现“Straßenbahn”→“Strassenbahn”(ß被错误替换为ss)等数据污染。

ICU MessageFormat驱动的本地化重构

某跨境电商后台系统在2022年Q3启动重构,将所有标题渲染层替换为@formatjs/intl-messageformat,配合CLDR v41数据源。关键变更包括:

  • 动态加载区域设置资源包(如de-DEzh-Hans-CN);
  • 使用{name, capitalize}占位符语法替代JS字符串操作;
  • 集成Intl.Segmenter处理阿拉伯语词边界(如“السلام عليكم”需按语义分段而非空格)。

多语言术语一致性校验流水线

构建CI/CD阶段的自动化校验规则: 检查项 工具 触发条件
术语表匹配 i18n-termbase-validator 提交包含en.json且未同步更新ja.json
大小写策略冲突 自定义ESLint插件 检测titleCase函数调用且无locale参数
RTL布局溢出 Puppeteer截图比对 阿拉伯语标题宽度>容器110%

字体与排版链路的协同治理

某新闻App在支持印地语(Devanagari)时发现:系统默认Noto Sans字体虽支持Unicode,但标题行高计算错误。解决方案包括:

  • 在CSS中为lang="hi"声明line-height: 1.45(非默认1.2);
  • 使用@font-face引入Noto Serif Devanagari Bold变体;
  • 通过getComputedStyle(el).fontFamily动态检测回退字体并触发告警。
flowchart LR
A[源文本 en-US] --> B[ICU Rule Engine]
B --> C{locale=zh-Hans?}
C -->|是| D[应用GB/T 15834-2011标题规范]
C -->|否| E[应用CLDR capitalization rules]
D --> F[输出“中华人民共和国宪法”]
E --> G[输出“Constitution Of The United States”]

机器翻译后编辑(MTPE)质量门禁

接入DeepL API时发现其返回的西班牙语标题存在冠词冗余(如“La Constitución de los Estados Unidos”应简化为“Constitución de Estados Unidos”)。团队开发正则过滤器:

const spanishTitleCleaner = (str) => 
  str.replace(/^La?\s+(Constitución|Ley|Decreto)\s+de\s+/i, '$1 de ');

该规则集成至Git Hook,在i18n/es-ES.json提交前自动执行,并标记修改行供人工复核。

文本治理的组织级能力建设

某银行核心系统建立跨职能i18n委员会,每月审查三类数据:

  • 本地化覆盖率热力图(按模块统计已翻译key占比);
  • 用户投诉TOP5语言问题(如韩语“서울특별시”在iOS Safari中显示为方块);
  • A/B测试结果(对比ICU vs 原生toLocaleUpperCase性能差异,Chrome 119下平均耗时差23ms)。

可访问性与国际化交叉验证

针对视障用户屏幕阅读器需求,强制要求所有标题元素添加aria-label属性,并通过axe-core扫描验证:

  • 英文标题<h1>Account Settings</h1>aria-label="English account settings"
  • 日文标题<h1>アカウント設定</h1>aria-label="Japanese account settings"
  • 扫描失败项自动阻断PR合并,错误示例:<h1 lang="ko">계정 설정</h1>缺失aria-label。

传播技术价值,连接开发者与最佳实践。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注