PGlite 自定义 ICU 语言包生成指南:为嵌入式 Postgres 构建精简本地化数据
【免费下载链接】pgliteEmbeddable Postgres with real-time, reactive bindings.项目地址: https://gitcode.com/GitHub_Trending/pg/pglite
导读
本文基于 PGlite 仓库中的 pglite-icu-full 示例文档 展开,完整讲解如何从 libicu 源码出发,借助 ICU Data Build Tool 生成"只包含你需要的 locale"的精简数据包(.tgz),并将其通过icuDataDir选项注入 PGlite,让嵌入式 Postgres 获得完整、正确的本地化排序、大小写转换与索引能力。读完本文,你将掌握从"下载 ICU 源码"到"构建、打包、加载自定义 locale 数据"的完整闭环,并能参考仓库中的瑞士(Switzerland)示例,为自己的应用裁剪出体积最小的 ICU 数据。
背景:为什么 PGlite 需要独立的 ICU 数据包
PGlite 是运行在 WebAssembly 中的嵌入式 Postgres,其本地化能力依赖 libicu。仓库文档 localesupport.md 明确说明:PGlite 通过libicu使用标准的 ISO C 与 POSIX locale 设施,而完整的 ICU 数据资源由单独分发的@electric-sql/pglite-icu-full包提供。
@electric-sql/pglite-icu-full的定位(见 package.json)是:包含 libicu 全部数据资源的包,用于构建本地化应用。它的入口实现位于 src/index.ts:
import { pglUtils } from '@electric-sql/pglite-utils' export async function icuDataDir(): Promise<Blob> { const moduleUrl = new URL('../dist/icu.76.tgz', import.meta.url) if (pglUtils.IN_NODE) { const fs = await import('fs/promises') const buffer = await fs.readFile(moduleUrl) return new Blob([new Uint8Array(buffer)]) } else { const downloadPromise = await fetch(moduleUrl) return downloadPromise.blob() } }该函数在 Node 环境直接读取dist/icu.76.tgz,在浏览器环境则通过fetch拉取,最终都归一化为Blob返回——这正是 PGliteicuDataDir选项所需的输入类型。静态数据文件位于 static/icu.76.tgz,构建脚本build: tsup && cp static/* ./dist/(见 package.json)会把它复制进dist目录随包发布。
使用完整包非常简单:
npm install @electric-sql/pglite-icu-full # 或 yarn add @electric-sql/pglite-icu-full # 或 pnpm add @electric-sql/pglite-icu-fullimport { PGlite } from '@electric-sql/pglite' import { icuDataDir } from '@electric-sql/pglite-icu-full' // 创建携带完整 ICU 资源的 PGlite 实例 const pg = await PGlite.create({ icuDataDir: await icuDataDir(), }) // 示例:查询当前可用的所有 collation const collations = await pg.exec('select * from pg_collation')但完整包"might be quite large"(可能相当大)——它加载的是 libicu 提供的全部 locale 集合。对于只面向少数几个语言区域的 Web 应用,这会造成不必要的体积浪费。这正是本文核心主题的由来:如何生成只包含所需 locale 的自定义 ICU 数据包。
PGlite 侧如何消费 ICU 数据:icuDataDir的底层实现
在深入构建流程之前,先看 PGlite 主包是如何接收并应用这个数据包的,这有助于理解最终产物的格式要求。
icuDataDir是 PGliteOptions 中的一个可选配置项,类型为Blob | File。在 pglite.ts 中,PGlite 实例创建后若检测到该选项,会立即调用私有方法#fillIcuDataDir:
// packages/pglite/src/pglite.ts#L660-L668 async #fillIcuDataDir(icuDataDir: Blob | File) { this.#log( `pglite: icuDataDir specified, removing default icu data dir at ${ICU_DATA_PATH}`, ) pglUtils.rmdirRecursive(this.mod!.FS, ICU_DATA_PATH) this.#log(`pglite: loading icu data from tarball ${icuDataDir}`) this.mod!.FS.mkdirTree(ICU_DATA_PATH) await loadTar(this.mod!.FS, icuDataDir, ICU_DATA_PATH) }关键点如下:
- 默认 ICU 数据目录被整体移除:
ICU_DATA_PATH定义在 initdb.ts 中,即PG_ROOT + '/icu'。也就是说,你提供的自定义数据会替换内置的默认 ICU 数据,而不是叠加。 - 数据以 tarball 形式加载:
loadTar会把传入的Blob/File解包到ICU_DATA_PATH下,这解释了为什么最终的归档文件必须是 tar/tgz 格式,且内部目录结构要与默认布局一致(如icudt76l/)。 ICU_DATA环境变量指向该目录:在 initdb 实例的preRun阶段(initdb.ts)和主模块中(pglite.ts),都会设置mod.ENV.ICU_DATA = ICU_DATA_PATH,让 libicu 从该目录读取数据。
因此,你生成的icu_*.tgz本质上是一个遵循 libicu 文件数据布局(file data packaging)的 tar 归档,PGlite 会在启动时把它解压进虚拟文件系统的 ICU 数据目录。
第一步:下载 libicu 源码与数据
PGlite 当前测试基于 libicu v76.1。需要同时下载两部分:
wget https://github.com/unicode-org/icu/releases/download/release-76-1/icu4c-76_1-src.tgz wget https://github.com/unicode-org/icu/releases/download/release-78.3/icu4c-78.3-data.zip注意:原文档中的第二个链接下载的是ICU Data Build Tool 对应版本的数据包(
icu4c-*-data.zip),它并非与 76.1 源码同版本,而是与你要使用的 Data Build Tool 版本配套。下载后需解压并替换源码目录中的data目录(详见下文注意事项)。
⚠️ 重要注意事项(务必逐条核对):
- 使用ICU Data Build Tool的前提是必须拥有数据源。解压
icu4c-76_1-src.tgz后,请检查icu4c/source/data/locales/root.txt是否存在; - 如果
root.txt缺失,需要下载icu4c-*-data.zip,删除旧的icu4c/source/data目录,并用 zip 包中的 data 目录替换它; - 如果
icu4c/source/data/in目录下存在*.dat文件,即使你提供了自定义 filter 规则,该 .dat 文件仍会被优先使用。这意味着你设置的 locale 过滤将不会生效——构建前必须确保该目录干净(不含旧.dat文件)。
第二步:编写 filters.json 过滤规则
ICU Data Build Tool 通过ICU_DATA_FILTER_FILE环境变量指向的 JSON 文件来决定哪些数据要保留、哪些要剔除。这是实现"只打包所需 locale"的核心机制。
最简单的 locale 过滤示例(只保留en_US):
{ "localeFilter": { "filterType": "locale", "includelist": ["en_US"] } }字段说明:
filterType:过滤类型,值为"locale"时按 locale 名称过滤;includelist:需要包含的 locale 标识列表(如"en_US"、"de_CH");- 除此之外,ICU 还支持
excludelist、includeChildren等选项,以及featureFilters(按功能模块过滤,例如剔除断词器、时区数据等),可以进一步压缩体积。
仓库中的瑞士示例 filters_switzerland.json 给出了一个生产级的完整配置:
{ "localeFilter": { "filterType": "locale", "includelist": ["root", "de_CH", "fr_CH", "it_CH", "rm"], "includeChildren": false }, "featureFilters": { "brkitr_rules": "exclude", "brkitr_dictionaries": "exclude", "brkitr_tree": "exclude", "conversion_mappings": "exclude", "confusables": "exclude", "curr_supplemental": "exclude", "curr_tree": "exclude", "lang_tree": "exclude", "normalization": "exclude", "region_tree": "exclude", "rbnf_tree": "exclude", "stringprep": "exclude", "zone_tree": "exclude", "translit": "exclude", "unames": "exclude", "ulayout": "exclude", "uemoji": "exclude", "unit_tree": "exclude", "cnvalias": "exclude" }, "collationUCAData": "implicithan" }这个配置体现了两个关键实践:
- localeFilter:
includelist只保留瑞士四种官方语言 locale(de_CH德语、fr_CH法语、it_CH意大利语、rm罗曼什语)加root根 locale(作为回退基础),并设置includeChildren: false不引入子 locale; - featureFilters:把断词器(brkitr)、货币(curr)、语言树(lang)、时区(zone)、转写(translit)、emoji(uemoji)、归一化(normalization)等与排序/大小写无关的功能模块全部
exclude,进一步削减体积——排序与大小写转换只需要 collation 相关的 UCA 数据。
结合测试用例可以验证:使用该瑞士数据包后,icuDataDir.test.ts 断言pg_collation中包含了de-CH-x-icu、fr-CH-x-icu、it-CH-x-icu,说明过滤后的数据足以支撑这些 locale 的排序与大小写功能。
第三步:构建 ICU(应用过滤器并编译)
写好 filters.json 后,进入 ICU 源码目录执行配置与编译。ICU_DATA_FILTER_FILE环境变量指向你 filter 文件的绝对路径:
ICU_DATA_FILTER_FILE=<full_path_to_your_filters.json> ./icu/source/configure \ --with-data-packaging=files \ --disable-shared \ --enable-static \ --disable-tests \ --disable-samples \ --disable-extras \ --disable-icuio \ --disable-layoutex \ --prefix=<your_install_dir> make -j && make install各配置项的作用与取舍:
| 配置项 | 作用 |
|---|---|
--with-data-packaging=files | 关键选项。将 ICU 数据以松散文件形式安装(而非打包成单个.dat文件),这是后续tar归档的前提;也规避了".dat文件会覆盖 filter 规则"的问题 |
--disable-shared --enable-static | 只构建静态库,适合 WASM 嵌入式场景 |
--disable-tests --disable-samples | 跳过测试与示例,加速构建 |
--disable-extras --disable-icuio --disable-layoutex | 剔除附加工具、ICU I/O 层与旧版布局引擎,进一步裁剪 |
--prefix=<your_install_dir> | 指定安装目录,后续打包只取其中的数据子目录 |
make -j并行编译,make install会把编译产物与过滤后的数据安装到--prefix指定的目录。
第四步:将 ICU 数据打成 tar 归档
安装目录中包含了所有与 ICU 相关的内容,但 PGlite只需要数据文件。数据位于安装目录的share/icu/76.1/下,打包如下:
cd <your_install_dir>/share/icu/76.1/ && tar cvfz icu_76.tgz icudt76l/此时生成的icu_76.tgz就是可以直接交给 PGlite 的本地化数据包。注意:
- 版本号路径
76.1必须与 PGlite 测试的 libicu 版本一致; - 归档根目录下的
icudt76l/目录名是 libicu 按版本生成的(76对应 ICU 76.x,l表示 little-endian 小端序),不要改名——PGlite 解包时依赖这套默认布局; - 用
tar cvfz(gzip 压缩)可以在保留目录结构的同时显著减小体积。
第五步:把自定义归档交给 PGlite
生成归档后,用它与仓库中@electric-sql/pglite-icu-full同等的加载方式注入 PGlite(Node 侧示例):
import { PGlite } from '@electric-sql/pglite' import { readFile } from 'fs/promises' import { resolve } from 'path' // 读取你生成的 icu 归档 const icuDataDir = await readFile( resolve(import.meta.dirname, 'icu_76.tgz'), ) // 包装成 Blob 后传给 PGlite const pg = await PGlite.create({ icuDataDir: new Blob([new Uint8Array(icuDataDir)]), })在浏览器/Worker 环境,也可以用fetch('icu_76.tgz').then(r => r.blob())直接得到 Blob。PGlite 会像上文分析的#fillIcuDataDir那样,移除默认 ICU 数据目录并解包你的归档。
实战验证:瑞士自定义语言包能做到什么
仓库在 examples/Switzerland/ 目录下同时提供了过滤配置 filters_switzerland.json 和生成好的数据文件icu_76_ch.tgz,可直接用于 PGlite。其配套测试 icuDataDir.test.ts 展示了该自定义包能提供的完整能力:
1. locale 感知的排序
-- 德语(瑞士):ä 紧跟在 a 附近排序 SELECT val FROM (VALUES ('Birne'), ('Apfel'), ('Ärger'), ('Banane')) AS t(val) ORDER BY val COLLATE "de-CH-x-icu"; -- 期望:'Ärger' 排在 'Banane' 之前 -- 法语(瑞士):带重音字符正确排序 SELECT val FROM (VALUES ('côte'), ('coté'), ('cote'), ('côté')) AS t(val) ORDER BY val COLLATE "fr-CH-x-icu"; -- 期望:'cote' 排在最前 -- 意大利语(瑞士):重音元音正确排序 SELECT val FROM (VALUES ('perché'), ('pera'), ('perciò'), ('percorso')) AS t(val) ORDER BY val COLLATE "it-CH-x-icu"; -- 期望:'pera' 排在 'perché' 之前2. 语言相关的大小写转换
SELECT lower('GÉNÈVE' COLLATE "fr-CH-x-icu") AS lo, -- 结果:génève upper('génève' COLLATE "fr-CH-x-icu") AS up; -- 结果:GÉNÈVE SELECT lower('ZÜRICH' COLLATE "de-CH-x-icu") AS lo, -- 结果:zürich upper('zürich' COLLATE "de-CH-x-icu") AS up; -- 结果:ZÜRICH3. 自定义 ICU collation 属性
-- 德语电话簿排序:ö 按 "oe" 展开 CREATE COLLATION IF NOT EXISTS ch_de_phonebook (provider = icu, locale = 'de-CH@collation=phonebook'); SELECT 'Goldmann' > 'Götz' COLLATE ch_de_phonebook AS phone; -- 结果:true -- 数字感知排序 CREATE COLLATION IF NOT EXISTS ch_numeric (provider = icu, locale = 'de-CH@colNumeric=yes'); SELECT val FROM (VALUES ('Haus-9'), ('Haus-10'), ('Haus-2'), ('Haus-1')) AS t(val) ORDER BY val COLLATE ch_numeric; -- 期望顺序:Haus-1, Haus-2, Haus-9, Haus-10 -- 不区分大小写的非确定性 collation CREATE COLLATION IF NOT EXISTS ch_ci (provider = icu, locale = 'de-CH@colStrength=secondary', deterministic = false); SELECT 'Grüezi' COLLATE ch_ci = 'grüezi' COLLATE ch_ci AS eq; -- 结果:true这些测试用例说明:即便过滤掉了时区、货币、emoji 等大量功能模块,排序(collation)与大小写(case conversion)能力依然完好——这正是业务应用最依赖的本地化能力,也是"裁剪体积不裁剪功能"的最佳佐证。
全量 ICU 与精简 ICU 的对比验证
仓库测试 icuDataDir.test.ts 还直观对比了默认 PGlite 与加载全量 ICU 数据包后的差异:
- 默认实例
pg_collation返回的 collation 数量 ≥ 10; - 加载
dist/icu.76.tgz全量包后,collation 数量≥ 879,且明显多于默认实例。
这从测试层面证实了两点:一是全量 ICU 包显著扩充了可用 locale 数量(879+ 个 collation);二是icuDataDir的替换机制生效——默认内置数据被完整替换为外部数据。对自定义精简包(如瑞士示例)而言,虽然 collation 数量少于全量包,但体积更小、加载更快,适合对包体积敏感的嵌入式与浏览器场景。
关键注意事项汇总
- 版本匹配:PGlite 当前针对 libicu v76.1 测试,构建时版本号路径(
share/icu/76.1/)与归档目录名(icudt76l/)需与该版本一致; - 数据源必须完整:确保
icu4c/source/data/locales/root.txt存在,缺失时用icu4c-*-data.zip的 data 目录替换,否则 ICU Data Build Tool 无法工作; - 警惕残留 .dat 文件:
icu4c/source/data/in下的任何*.dat都会绕过你的 filter 规则被直接采用,构建前务必清理; - 打包格式:
--with-data-packaging=files是必须的,产物必须是包含icudt76l/目录结构的 tar/tgz,因为 PGlite 侧通过loadTar解包到ICU_DATA_PATH(PG_ROOT/icu); - 替换而非叠加:传入
icuDataDir后,PGlite 会先递归删除默认 ICU 数据目录再解包你的归档(见 pglite.ts),因此你的包需要自带完整可用的数据(通常应包含rootlocale 作为回退基础,参考瑞士示例); - 体积与功能的平衡:通过
featureFilters剔除排序无关的模块(时区、货币、emoji、转写等)可显著减小包体积,且不影响 collation 与大小写能力——瑞士示例的 filters_switzerland.json 是可直接复用的参考模板。
参考文档
- 本文主体来源:pglite-icu-full/examples/README.md
- 完整 ICU 包使用说明:pglite-icu-full/README.md
- PGlite 官方本地化文档:docs/docs/localesupport.md
- ICU 数据加载实现:packages/pglite/src/pglite.ts、packages/pglite/src/initdb.ts
icuDataDir选项定义:packages/pglite/src/interface.ts- 瑞士示例过滤配置:packages/pglite-icu-full/examples/Switzerland/filters_switzerland.json
- 功能验证测试:packages/pglite-icu-full/tests/icuDataDir.test.ts
【免费下载链接】pgliteEmbeddable Postgres with real-time, reactive bindings.项目地址: https://gitcode.com/GitHub_Trending/pg/pglite
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考