news 2026/9/14 17:15:28

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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}设置文档语言,并据此解析Englishitfr-CA这类 locale 标识符。为了在运行时彻底摆脱对 JDKjava.util.Locale的依赖、保证任何平台上的结果都确定且一致,仓库用 quarkdown-locale-table-processor 这个 KSP 处理器,在构建期把构建 JDK 的 CLDR 语言数据抽取成两张 Kotlin 常量表(LanguagesTerritories)编译进核心库。读完本文,你将掌握这个处理器的完整运行机制、生成产物形态、它在 Quarkdown 本地化体系中的调用位置,以及如何把它接入自己的 Kotlin 项目。

一、模块定位:为什么需要在构建期生成 locale 表

1.1 要解决的问题

Quarkdown 的文档元数据支持用.doclang指定文档语言,取值可以是不区分大小写的英文全名EnglishItalianFrench (Canada))或IETF BCP 47 语言标签enitfr-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 语言代码itItalian
TerritoriesISO 3166 国家/地区代码ITItaly

从源码看,两张表正是由 LocaleTableCodeGenerator.kt 中的tables列表声明,输出属性名分别为LanguagesTerritories,KDoc 分别为 “Languages by ISO 639 code” 和 “Territories by ISO 3166 country code”。

二、处理器内部实现剖析

该模块只包含三个 Kotlin 源文件加一个服务注册文件,分工非常清晰。

2.1 入口:LocaleTableSymbolProcessorProvider

LocaleTableSymbolProcessorProvider.kt 实现 KSP 的SymbolProcessorProvider接口,负责实例化处理器。KSP 通过SPI(服务提供者接口)机制发现它,注册文件位于 META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider,其内容仅一行:

com.quarkdown.processor.locale.LocaleTableSymbolProcessorProvider

2.2 处理器:LocaleTableSymbolProcessor

LocaleTableSymbolProcessor.kt 是核心处理器。值得注意的设计点是:它不读取任何源码符号——既没有注解、也没有待处理的类型,而是在首个process轮次无条件地生成唯一输出文件:

override fun process(resolver: Resolver): List<KSAnnotated> { 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 代码生成器:LocaleTableCodeGenerator

LocaleTableCodeGenerator.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()排序后,按codesnames两个并行listOf字面量输出为internal val声明,代码注释特别强调排序是为了配合NameTable的二分查找;文件头部还会生成一行“Generated at build time … Do not edit.”的防误改提示。

三、生成产物:LocaleTables.ktNameTable

3.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):查代码对应的英文名,缺失返回null
  • codeOf(name):按英文名不区分大小写反查代码(equals(name, ignoreCase = true)),这也正是.doclang {English}大小写不敏感特性的底层来源。

由于codes有序,所有查询均为 O(log n) 二分查找。

四、在 Quarkdown 运行时中的消费链路

4.1LocaleLoader抽象与默认实现

LocaleLoader.kt 定义 locale 检索接口:

  • all:所有受支持的基础语言 locale(不含地区变体);
  • fromTag(tag):按标签解析,如enen-USitfr-CA
  • fromName(name):按英文名解析,如EnglishItalianFrench (Canada)
  • find(identifier):先按名称、再按标签解析的兜底方法。

其伴生对象SYSTEM明确指向表驱动的默认实现:

val SYSTEM: LocaleLoader get() = TableLocaleLoader

4.2TableLocaleLoader:如何用两张表解析

TableLocaleLoader.kt 是两张表的直接消费者:

  • all遍历Languages.codes,构造不带地区的基础 locale;
  • fromTag:拆分-子标签,语言子标签小写后经Languages.contains校验,地区则取第一个能命中Territories的子标签(大写化后);
  • fromName:经LocaleDisplayName.split拆出语言名与可选地区名,分别用Languages.codeOfTerritories.codeOf反查。

4.3TableLocaleLocaleDisplayName

TableLocale.kt 实现Locale接口,displayNamecheckNotNull(Languages.nameOf(code))从表中取英文名,组合出tag(如en-US)与shortTag(如en)。

LocaleDisplayName.kt 定义了Language (Territory)展示名格式的双向能力:format用于拼装(如French (Canada)),split用于解析(对含括号的英文地区名,如Cocos (Keeling) Islands,按首尾(… )配对切分,保证往返一致)。

4.4 完整解析链

TableLocaleLoaderNameTableLanguages/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 对表驱动解析做了系统验证,可作为行为契约:

测试点断言
默认检索器TableLocaleLoaderLocaleLoader.SYSTEM
English/Italian标签、全名、大小写变体(eNgLiShiTaLiAn)解析一致,displayName正确
en-US/fr-CA标签与全名(English (United States)French (Canada))双向一致,countryCode正确
含括号地区名en-CCdisplayNameEnglish (Cocos (Keeling) Islands),且可往返解析回原 locale
CJKzh/Chinese/ja/ko均命中且isCJK()为真
非法输入fromTag/fromName/findnonexistent均返回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 数据”的模式,可以照搬本模块的三层结构:

  1. Provider:实现SymbolProcessorProvider,在META-INF/services中注册全限定类名;
  2. Processor:持有CodeGenerator,首轮无条件输出(invoked防重);
  3. 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 17:14:36

SpringBoot医院管理系统开发与架构设计实践

1. 项目概述这个基于SpringBoot的医院管理系统是一个典型的医疗行业信息化解决方案&#xff0c;我去年为某三甲医院实施过类似项目。这类系统本质上是通过数字化手段重构传统医院管理模式&#xff0c;将挂号、问诊、药品管理等核心业务流程从线下搬到线上。SpringBoot的快速开发…

作者头像 李华
网站建设 2026/9/14 17:10:43

微网CVaR动态定价策略:风险量化与Matlab实现

1. 项目背景与核心价值 微网作为分布式能源系统的重要形态&#xff0c;其运营面临源-荷双重不确定性的挑战。传统确定性优化方法难以有效量化风光出力波动和负荷需求变化带来的财务风险&#xff0c;这正是CVaR&#xff08;Conditional Value at Risk&#xff09;风险度量工具的…

作者头像 李华
网站建设 2026/9/14 17:09:05

基于Matlab的暗通道先验图像去雾系统实现

1. 项目概述在计算机视觉领域&#xff0c;图像去雾技术一直是个既有趣又实用的研究方向。今天我要分享的是基于Matlab实现的暗通道先验图像去雾系统&#xff0c;这个项目不仅包含了核心的去雾算法实现&#xff0c;还配备了完整的GUI界面&#xff0c;让用户可以直观地调整参数并…

作者头像 李华
网站建设 2026/9/14 17:04:29

LangChain消息队列优化:提升AI应用响应速度与并发能力

1. 项目背景与核心价值在AI应用开发领域&#xff0c;LangChain作为当前最流行的LLM应用框架之一&#xff0c;其前端消息队列的实现直接关系到用户体验和系统稳定性。传统聊天界面常见的"消息堆积"、"响应卡顿"问题&#xff0c;本质上都是消息处理机制设计不…

作者头像 李华