ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Quarkdown 的 locale-table-processor:用 KSP 在编译期把 JDK Locale 数据固化为运行时语言表

2026/9/14 17:15:42 拓冰建站 浏览量
Quarkdown 的 locale-table-processor:用 KSP 在编译期把 JDK Locale 数据固化为运行时语言表 Quarkdown 的 locale-table-processor用 KSP 在编译期把 JDK Locale 数据固化为运行时语言表【免费下载链接】quarkdown Markdown with superpowers: from ideas to papers, presentations, websites, books, and knowledge bases.项目地址: https://gitcode.com/GitHub_Trending/qu/quarkdown导读Quarkdown 文档里用.doclang {locale}设置文档语言并据此解析English、it、fr-CA这类 locale 标识符。为了在运行时彻底摆脱对 JDKjava.util.Locale的依赖、保证任何平台上的结果都确定且一致仓库用 quarkdown-locale-table-processor 这个 KSP 处理器在构建期把构建 JDK 的 CLDR 语言数据抽取成两张 Kotlin 常量表Languages与Territories编译进核心库。读完本文你将掌握这个处理器的完整运行机制、生成产物形态、它在 Quarkdown 本地化体系中的调用位置以及如何把它接入自己的 Kotlin 项目。一、模块定位为什么需要在构建期生成 locale 表1.1 要解决的问题Quarkdown 的文档元数据支持用.doclang指定文档语言取值可以是不区分大小写的英文全名English、Italian、French (Canada)或IETF BCP 47 语言标签en、it、fr-CA参见 docs/document-metadata.qd。本地化功能依赖这一能力它决定内容本地化使用的目标 locale、中文等 locale 的专属字体样式以及 HTMLlang属性。但直接依赖 JDK 的java.util.Locale有两大痛点平台不一致不同 JDK 发行版、不同 CLDR 版本提供的 locale 展示名可能不同导致同一份文档在不同机器上解析出不同结果运行期耦合CLI、LSP、服务器等组件在运行时仍需 JDK 的 locale 数据增加了运行环境约束。locale-table-processor的答案是在编译期完成数据抽取把结果固化进产物。正如 README 所述将这份数据打包进核心库后.doclang使用的 locale 解析在运行时变得平台无关且确定不再依赖 JDK。1.2 产出两张表处理器构建期生成两张“代码 → 英文名”映射表表依据标准示例LanguagesISO 639 语言代码it→ItalianTerritoriesISO 3166 国家/地区代码IT→Italy从源码看两张表正是由 LocaleTableCodeGenerator.kt 中的tables列表声明输出属性名分别为Languages和TerritoriesKDoc 分别为 “Languages by ISO 639 code” 和 “Territories by ISO 3166 country code”。二、处理器内部实现剖析该模块只包含三个 Kotlin 源文件加一个服务注册文件分工非常清晰。2.1 入口LocaleTableSymbolProcessorProviderLocaleTableSymbolProcessorProvider.kt 实现 KSP 的SymbolProcessorProvider接口负责实例化处理器。KSP 通过SPI服务提供者接口机制发现它注册文件位于 META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider其内容仅一行com.quarkdown.processor.locale.LocaleTableSymbolProcessorProvider2.2 处理器LocaleTableSymbolProcessorLocaleTableSymbolProcessor.kt 是核心处理器。值得注意的设计点是它不读取任何源码符号——既没有注解、也没有待处理的类型而是在首个process轮次无条件地生成唯一输出文件override fun process(resolver: Resolver): ListKSAnnotated { if (!invoked) { invoked true codeGenerator .createNewFile(Dependencies(aggregating false), PACKAGE_NAME, FILE_NAME) .bufferedWriter() .use { it.write(LocaleTableCodeGenerator().buildSource()) } } return emptyList() }invoked标志保证文件只生成一次避免多轮处理重复写入Dependencies(aggregating false)声明产物不聚合依赖任何源文件只要处理器运行即可生成process恒返回空列表即不延迟处理任何符号输出包名com.quarkdown.core.localization.table、文件名LocaleTables由LocaleTableCodeGenerator中的常量定义。2.3 代码生成器LocaleTableCodeGeneratorLocaleTableCodeGenerator.kt 负责“抽取数据 → 生成源码文本”。语言表的抽取逻辑languages()val codes Locale.getISOLanguages().toSet() Locale.getAvailableLocales().mapNotNull { it.language.takeIf(String::isNotBlank) } return codes.namesBy { Locale(it).getDisplayLanguage(Locale.ENGLISH) }语言代码集合是ISO 639 代码与 JDK 可用 locale 的语言代码的并集后者会补充yue这类三字母代码。源码用Suppress(DEPRECATION)注释解释了为何使用Locale(String)构造器它保留iw等旧代码而forLanguageTag会将其规范化。地区表的抽取逻辑territories()Locale.getISOCountries().toSet().namesBy { Locale.Builder().setRegion(it).build().getDisplayCountry(Locale.ENGLISH) }数据清洗namesBy会把「展示名为空」或「展示名与代码相同」的条目剔除避免XX这类无意义的映射污染表。生成格式数据以toSortedMap()排序后按codes与names两个并行listOf字面量输出为internal val声明代码注释特别强调排序是为了配合NameTable的二分查找文件头部还会生成一行“Generated at build time … Do not edit.”的防误改提示。三、生成产物LocaleTables.kt与NameTable3.1 生成源码形态处理器生成的LocaleTables.kt包名com.quarkdown.core.localization.table大致如下示意实际由构建 JDK 决定数据// Generated at build time by the quarkdown-locale-table-processor KSP processor. Do not edit. package com.quarkdown.core.localization.table /** * Languages by ISO 639 code. */ internal val Languages: NameTable NameTable( codes listOf( aa, ab, ... ), names listOf( Afar, Abkhazian, ... ), ) /** * Territories by ISO 3166 country code. */ internal val Territories: NameTable NameTable( codes listOf( AD, AE, ... ), names listOf( Andorra, United Arab Emirates, ... ), )3.2NameTable面向二分查找的只读索引消费端 NameTable.kt 定义了一个基于「有序并行列表」的只读索引提供三个操作contains(code)codes.binarySearch(code) 0用于判断代码是否存在nameOf(code)查代码对应的英文名缺失返回nullcodeOf(name)按英文名不区分大小写反查代码equals(name, ignoreCase true)这也正是.doclang {English}大小写不敏感特性的底层来源。由于codes有序所有查询均为 O(log n) 二分查找。四、在 Quarkdown 运行时中的消费链路4.1LocaleLoader抽象与默认实现LocaleLoader.kt 定义 locale 检索接口all所有受支持的基础语言 locale不含地区变体fromTag(tag)按标签解析如en、en-US、it、fr-CAfromName(name)按英文名解析如English、Italian、French (Canada)find(identifier)先按名称、再按标签解析的兜底方法。其伴生对象SYSTEM明确指向表驱动的默认实现val SYSTEM: LocaleLoader get() TableLocaleLoader4.2TableLocaleLoader如何用两张表解析TableLocaleLoader.kt 是两张表的直接消费者all遍历Languages.codes构造不带地区的基础 localefromTag拆分-子标签语言子标签小写后经Languages.contains校验地区则取第一个能命中Territories的子标签大写化后fromName经LocaleDisplayName.split拆出语言名与可选地区名分别用Languages.codeOf、Territories.codeOf反查。4.3TableLocale与LocaleDisplayNameTableLocale.kt 实现Locale接口displayName用checkNotNull(Languages.nameOf(code))从表中取英文名组合出tag如en-US与shortTag如en。LocaleDisplayName.kt 定义了Language (Territory)展示名格式的双向能力format用于拼装如French (Canada)split用于解析对含括号的英文地区名如Cocos (Keeling) Islands按首尾(… )配对切分保证往返一致。4.4 完整解析链TableLocaleLoader→NameTableLanguages/Territories→TableLocale构成了LocaleLoader.SYSTEM的完整链路再往上是 ContextLocalization.kt 中通过LocaleLoader.SYSTEM.fromTag(en)得到的默认 locale。.doclang的 locale 解析、docs/localization.qd描述的.localization本地化表键名、以及LocaleNotSetException提示“Tip:.doclang {locale}”见 LocalizationExceptions.kt最终都落在这两张编译期生成的表上。五、测试验证确定性行为的证据LocaleTest.kt 对表驱动解析做了系统验证可作为行为契约测试点断言默认检索器TableLocaleLoader即LocaleLoader.SYSTEMEnglish/Italian标签、全名、大小写变体eNgLiSh、iTaLiAn解析一致displayName正确en-US/fr-CA标签与全名English (United States)、French (Canada)双向一致countryCode正确含括号地区名en-CC的displayName为English (Cocos (Keeling) Islands)且可往返解析回原 localeCJKzh/Chinese/ja/ko均命中且isCJK()为真非法输入fromTag/fromName/find对nonexistent均返回null全量加载all序列非空六、接入自己的 Kotlin 项目6.1 构建配置处理器已注册进主构建settings.gradle.kts 中include(quarkdown-locale-table-processor)。在消费方quarkdown-core中仅需在 build.gradle.kts 添加一行 KSP 依赖plugins { kotlin(jvm) id(com.google.devtools.ksp) } dependencies { ksp(project(:quarkdown-locale-table-processor)) }构建时KSP 会自动发现LocaleTableSymbolProcessorProvider并运行处理器生成的LocaleTables.kt出现在build/generated/ksp/main/kotlin下随编译进入产物。6.2 可复用的三件套若要复刻这套“编译期固化 JDK 数据”的模式可以照搬本模块的三层结构Provider实现SymbolProcessorProvider在META-INF/services中注册全限定类名Processor持有CodeGenerator首轮无条件输出invoked防重CodeGenerator用构建 JDK 的java.util.Locale抽取数据 → 清洗 → 排序 → 生成 Kotlin 源码文本。6.3 注意事项数据源 构建 JDK表内容由执行构建的那台机器上的 JDK CLDR 数据决定因此团队内应统一构建 JDK 版本保证产物一致运行时零 JDK 依赖LocaleTables.kt是纯 Kotlin 常量NameTable只做二分查找运行环境包括 GraalVM native image 等受限场景无需任何java.util.Locale能力不要手改产物生成文件头部明确标注 “Do not edit.”任何修改都会在下次构建时被覆盖。七、小结locale-table-processor用不到三个源文件把「构建期数据抽取」与「运行时确定性」结合得很好LocaleTableSymbolProcessor驱动生成、LocaleTableCodeGenerator负责抽取与排版、NameTable提供二分查找索引最终由TableLocaleLoader支撑.doclang的 locale 解析。对于需要在多平台产物中固化系统数据的项目这是一个值得借鉴的 KSP 实践范式。【免费下载链接】quarkdown Markdown with superpowers: from ideas to papers, presentations, websites, books, and knowledge bases.项目地址: https://gitcode.com/GitHub_Trending/qu/quarkdown创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考