Lingui 自定义消息目录格式器:在 js-lingui 中编写 Custom Formatter 的完整实践 开发工具前端【免费下载链接】js-lingui A readable, automated, and optimized (2 kb) internationalization for JavaScript项目地址https://gitcode.com/gh_mirrors/js/js-lingui点击查看免费下载本文基于 js-lingui 官方指南《Creating a Custom Message Formatter》展开系统讲解CatalogFormatter接口的定义、parse/serialize双函数的职责边界以及如何直接在lingui.config.ts中内联实现一个自定义 catalog 格式器。读完后你能够为翻译平台或内部工具对接 Lingui 未原生支持的目录格式如专有 JSON 变体、TOML、YAML并理解FormatterWrapper在extract/compile流程中如何调度你的实现。一、什么是 Catalog Formatter何时需要自定义Lingui 的catalog format指的是离线目录文件的格式——也就是lingui extract写入、翻译方填写、lingui compile读取的那份文件。这个格式只在离线翻译工作流中使用编译阶段它会先被解析成内部CatalogType再转译成一个简单的 JS 模块供运行时消费。为什么离线格式需要可插拔因为翻译目录的格式取决于各自的本地化工作流有人用 PO 对接 Crowdin有人把 JSON 丢给翻译供应商也有人需要给内部平台导出一套专有结构。Lingui 内置了 PO默认、PO-gettext、JSON、CSV 四种格式器详见 catalog-formats 参考文档但当你的项目要求的格式不在其中时官方推荐的做法就是写一个自定义 formatter——它让你完整定义提取出的字符串如何被写入你的自定义目录格式为特殊工作流和独特文件结构提供灵活性。关键的一点是自定义 formatter 不需要单独发包。它就是一个普通对象可以直接写在lingui.config.{ts,js}里随配置文件一起被 CLI 加载。二、Formatter 接口两个函数 两个扩展名一个 formatter 本质上是一个具有parse和serialize两个函数的对象分别定义目录从你的格式读入和写回到你的格式的方式。官方给出的接口形状如下来自 custom-formatter.md与仓库中lingui/conf导出的 CatalogFormatter 类型 一致export type CatalogFormatter { catalogExtension: string; /** * Set extension used when extract to template * Omit if the extension is the same as catalogExtension */ templateExtension?: string; parse( content: string, ctx: { locale: string | null; sourceLocale: string; filename: string } ): PromiseCatalogType | CatalogType; serialize( catalog: CatalogType, ctx: { locale: string | null; sourceLocale: string; filename: string; existing: string | null } ): Promisestring | string; };各成员含义成员必填说明catalogExtension是目录文件扩展名extract生成locales/{locale}扩展名时直接使用它见 Catalog.getFilenametemplateExtension否模板文件extract-template命令的输出使用的扩展名省略时与catalogExtension相同。内置 PO 格式器就是典型例子目录为.po、模板为.potpo.tsparse是把目录文件的原始文本content: string解析为CatalogType可同步或异步serialize是把CatalogType序列化为目录文件文本可同步或异步两个函数的 ctx 上下文参数parse和serialize的第二个参数ctx提供了当前处理环境的元数据locale当前 catalog 的语言模板场景下可能为空sourceLocale配置中声明的源语言默认en可用于区分源语言 catalog与其他语言filename当前正在读写的目标文件路径existing仅serialize目标文件已存在时的原始文本内容不存在时为null。existing是格式器控制 diff 的关键抓手。以内置 JSON 格式器为例它在 serialize 中读取 existing 来决定输出是否保留结尾换行符——如果原文件没有换行输出也不加从而避免产生无关 diff。内置 PO 格式器更进一步它会解析 existing 中的 PO 头信息X-Generator、Language等并在重新序列化时尽量保持头字段顺序不变po.ts 中的 getExistingHeaders/getHeaderOrder这也是你写自定义格式器时值得借鉴的低扰动写入策略。三、CatalogType格式器读写的统一中间表示无论你的目标格式长什么样parse的返回值与serialize的入参都是同一个中间结构——CatalogType。在 custom-formatter.md 中它的定义是export type CatalogType { [msgId: string]: MessageType; }; type CatalogExtra Recordstring, unknown; export type MessageTypeExtra CatalogExtra { message?: string; origin?: MessageOrigin[]; comments?: string[]; obsolete?: boolean; context?: string; translation?: string; /** * the generic field where * formatters can store additional data */ extra?: Extra; };对应仓库中的 CatalogType / MessageType 定义其中MessageOrigin是[filename: string, line?: number]元组。逐字段理解键msgId消息 ID。可能是显式 ID也可能是由messagecontext生成的哈希 IDmessage源语言默认消息文本translation该语言下的译文——你的格式器通常只需要持久化它origin消息在源码中的出现位置文件 行号数组comments给译者的开发者注释obsolete标记已废弃源码中已删除但保留历史译文的消息contextmsgctxt用于同文本不同场景的消息消歧extra格式器私有的扩展字段泛型参数Extra让每种格式器可以存放自己的元数据而不污染通用字段。extra是自定义格式器最容易忽视、却非常实用的字段。内置 PO 格式器就通过它把自己的概念映射了回来——POCatalogExtra 定义了translatorComments#译者注释和flags如fuzzy两个专属字段deserialize时写入extraserialize时再从extra读回po.ts L421-L424。如果你的自定义格式有自己的译者注释区或flag 标记照这个模式做即可。四、在 lingui.config 中配置自定义 formatter官方指南给出的最简配置是直接内联在配置文件里import { defineConfig } from lingui/cli; export default defineConfig({ // [...] format: { catalogExtension: json, parse: (content: string): CatalogType JSON.parse(content), serialize: (catalog: CatalogType): string JSON.stringify(catalog), }, });几个要点format就是配置项本身。在 LinguiConfig.format 的类型声明中它被标注为CatalogFormatter未设置时默认回退到 PO 格式器——这一点可以从 CLI 的入口逻辑确认getFormat 在format为空时执行(await import(lingui/format-po)).formatter()。TypeScript 用户注意如果你用 TypeScript 编写 formatterLingui 配置文件也应使用.ts扩展名lingui.config.ts这是官方文档明确给出的注意事项否则类型无法正确解析。上面这个内联 JSON 例子把 catalog 全量 JSON 化输出。如果你想输出内置lingui/format-json支持的minimal 扁平风格{ id: translation }也可以直接引用官方包而非手写内置实现里 serializeMinimal/deserializeMinimal 展示了只保留译文、丢弃元数据的正反两个方向如何互相映射你的自定义格式完全可以参考这两个函数。一个稍微完整、带上下文感知的内联示例演示ctx的实际用法// lingui.config.ts import { defineConfig } from lingui/cli; import type { CatalogType } from lingui/conf; export default defineConfig({ locales: [en, cs], sourceLocale: en, format: { catalogExtension: json, // 读入把平台导出的 { id: 译文 } 扁平结构还原为 CatalogType parse: (content: string) { const flat JSON.parse(content) as Recordstring, string; const catalog: CatalogType {}; for (const [id, translation] of Object.entries(flat)) { catalog[id] { translation, obsolete: false, message: null, origin: [] }; } return catalog; }, // 写出仅序列化源语言目录时才附带 origin降低译者端 diff 噪声 serialize: (catalog: CatalogType, ctx) { const out: Recordstring, string {}; for (const [id, m] of Object.entries(catalog)) { out[id] m.translation ?? ; } return JSON.stringify(out, null, 2) \n; }, }, });注意parse的还原逻辑{ translation, obsolete: false, message: null, origin: [] }与内置 CSV 格式器的 deserialize 完全同构——任何只存译文的扁平格式都适用这套还原模板。五、FormatterWrapper你的 formatter 如何被 CLI 调度理解 CLI 如何调用格式器能让你写出更健壮的parse/serialize。入口在 FormatterWrapper读目录read先读文件文件不存在直接返回undefined首次 extract 的场景存在则调用f.parse(content, { locale, sourceLocale, filename })并包一层RethrownError(Cannot read filename)——你的 parse 抛出的任何错误都会被归因到具体文件所以抛错前给出可读信息很重要CSV 格式器就在此处把 papaparse 的 error 列表拼接成 Error 抛出见 csv.ts L17-L21。写目录write先读existing传给serialize然后比较序列化结果与原文件内容相同则跳过写盘formatterWrapper.ts L36-L47。这意味着你的serialize必须是幂等且确定性的同一 catalog 两次序列化出不同结果例如时间戳、随机顺序会导致每次extract都产生文件变更。扩展名解析getCatalogExtension()返回catalogExtensiongetTemplateExtension()返回templateExtension || catalogExtensionformatterWrapper.ts L11-L17。extract-template生成的模板文件名即由后者决定见 Catalog 构造函数中的 templateFile。模板与 catalog 的流转Catalog.make会并行执行collect从源码提取新消息与readAll用你的parse读回所有语言旧目录再 merge、排序、逐个writecatalog.ts make。所以parse对旧目录的容错质量直接决定增量提取是否正确——例如 PO 格式器在 deserialize 中做了 obsolete 冲突保护当同 ID 出现多条记录时非 obsolete 记录优先覆盖 obsolete 记录。对serialize的existing参数的使用内置 JSON 格式器是最小示范按原文件是否有尾换行决定输出PO 格式器是最大示范保留头信息与字段顺序。自定义格式器建议至少做到 JSON 格式器级别的尾换行感知。六、设计自定义格式器时的工程建议结合内置四个格式器PO / PO-gettext / JSON / CSV的源码可以归纳出几条经过验证的实践决定持久化哪些元数据并想好丢失后的还原方式。metadata 丢失不是致命的——内置 minimal JSON 和 CSV 都只存translationparse时用固定骨架obsolete: false, message: null, origin: []补齐即可。真正需要保留的是格式特有的状态译者注释、flag、obsolete 标记这类状态请放进extra并在parse/serialize中成对处理避免extract往返后丢失译者成果。serialize保持确定性利用 CLI 的内容不变不写盘优化见上节第 2 点来减少 git 噪音。善用sourceLocale区分源语言目录。PO 格式器在 serialize 中判断ctx.locale ctx.sourceLocale显式 ID 的源语言消息会把原文作为 msgstr 输出防止源文本彻底丢失。如果你的格式在源语言目录下需要不同结构这是现成的判断依据。templateExtension用来对齐翻译平台约定。平台若要求.pot式模板就设catalogExtension: potemplateExtension: pot若模板与 catalog 同构则省略。参考实现优先于凭空设计。四种内置格式器都在packages/format-*/src/下其中 format-json 最短约 130 行、含测试可对照 csv.test.ts 的测试思路format-po 展示了最完整的 existing 保留与 header 管理策略。七、内置 formatter 速查与自定义的边界格式器包扩展名适用场景PO默认lingui/format-po.po/.pot标准 gettext 生态元数据最全PO-gettextlingui/format-po-gettext.po平台不理解 ICU plural 时用 gettext 原生复数JSONlingui/format-json.jsonlingui全量元数据风格或minimal扁平风格CSVlingui/format-csv.csv表格工具导入导出两列ID 与文本自定义无需发包任意上述都不满足时的兜底方案需要自定义的边界判断很简单如果现有格式器加配置项能覆盖你的需求如 PO 的origins、lineNumbers、customHeaderAttributes等选项优先在format: formatter({...})里调参只有当文件结构本身不同换扩展名、换数据布局、对接专有平台 schema时才动手写CatalogFormatter。小结Lingui 的 catalog 格式可插拔机制把翻译工作流与运行时彻底解耦CLI 侧只认CatalogType这一中间表示格式器负责在任意自定义文本格式与它之间双向转换。你要做的全部事情就是实现一个{ catalogExtension, templateExtension?, parse, serialize }对象并放进lingui.config的format字段。配合ctx.existing做低扰动写入、用extra承载格式私有元数据、参考 format-json 与 format-po 的成对parse/serialize写法就能为任意翻译平台构建出稳定、可控、diff 友好的目录管线。相关源码入口CatalogFormatter 类型、FormatterWrapper 调度、格式回退逻辑、内置格式器实现 与 格式参考文档。赞分享开发工具前端【免费下载链接】js-lingui A readable, automated, and optimized (2 kb) internationalization for JavaScript项目地址https://gitcode.com/gh_mirrors/js/js-lingui点击查看免费下载相关推荐rMview与reMarkable生态集成如何与其他开源工具协同工作的终极指南rMview与reMarkable生态集成如何与其他开源工具协同工作的终极指南 如果你正在寻找一个 快速、高效的reMarkable屏幕实时查看器 那么rMjs-lingui lingui/format-po默认 PO 目录格式的深度解析与 PO 格式化器实现js lingui lingui/format po默认 PO 目录格式的深度解析与 PO 格式化器实现 本文以 packages/format po/RE开发工具前端js-lingui 在 Next.js SWC 编译器下的国际化实战lingui/swc-plugin 完整接入指南js lingui 在 Next.js SWC 编译器下的国际化实战lingui/swc plugin 完整接入指南 本文基于 js lingui 仓库开发工具前端上一篇从Google Authenticator迁移到Flipper Authenticator完整步骤与避坑指南下一篇cuda-convnet2与TensorFlow对比分析哪个更适合你的深度学习项目创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考