Ghost 国际化实践指南:基于 packages/i18n 的文案提取、翻译命名空间与 CI 校验

📅 发布时间:2026/9/8 22:31:24
Ghost 国际化实践指南:基于 packages/i18n 的文案提取、翻译命名空间与 CI 校验
Ghost 国际化实践指南基于 packages/i18n 的文案提取、翻译命名空间与 CI 校验【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/GhostGhost 采用一套以共享包 packages/i18n 为核心的国际化i18n体系从 Ghost Core、Portal、Comments、Signup form 与 Search 等可翻译面中提取英文源字符串生成多语言资源文件并统一提供给各端使用。本文面向需要在 Ghost 仓库中新增或修改界面/邮件产品文案的开发者完整讲解命名空间划分、t()翻译助手的正确写法、字符串提取与context.json上下文同步、以及lint:translations与包测试组成的 CI 校验闭环翻译与新增语言的流程另见 Translating Ghost。从源字符串到多语言资源的链路packages/i18n 是 Ghost monorepo 中唯一的翻译中枢包名tryghost/i18n。其工作方式可概括为一条流水线开发者在代码中通过t(英文句子)写入源字符串提取脚本i18next-parser扫描各命名空间对应的源码路径把字符串写入locales/locale/namespace.json并同步出上下文索引 locales/context.json运行期由 i18next 根据当前语言加载对应 locale 资源向 Ghost Core服务端与各 public apps浏览器端提供翻译实例。值得强调的是这套体系面向的是产品文案界面与会员邮件也就是本文讲解的五个可翻译命名空间主题theme内的自定义文案不走该提取链路而是另由运行期的主题 locale 机制处理。五个命名空间翻译文件的组织边界所有翻译文件都遵循同一目录约定packages/i18n/locales/locale/namespace.json例如荷兰语的 Portal 文案位于packages/i18n/locales/nl/portal.json。提取脚本在 package.json 中定义了五个命名空间及其对应的源码扫描范围Namespace命名空间覆盖范围提取脚本实际扫描的源码ghostGhost Core包括服务端、前端与会员邮件模板对应ghost/core/core/{frontend,server,shared}/**/*.{js,jsx,ts}以及 email-rendering、email-service、comments、member-welcome-emails、gifts 等邮件模板portalPortalapps/portal/src/**/*.{js,jsx,ts,tsx}commentsCommentsapps/comments-ui/src/**/*.{ts,tsx}signup-formSignup formapps/signup-form/src/**/*.{ts,tsx}searchSearchapps/sodo-search/src/**/*.{js,jsx,ts,tsx}每个 locale 目录下都恰好有这五个 JSON 文件这保证了任何一端都能在locales/下找到属于自己的那份翻译。语言清单本身由 lib/locale-data.json 定义每项是{ code: de-CH, label: Swiss German }形式的记录同时包含基础语言与de-CH、pt-BR、sr-Cyrl、zh-Hant等区域/文字变体。英文值为什么是空的Ghost 的翻译模型有一个关键约定传给t()的英文句子本身就是翻译键key。以 locales/en/ghost.json 为例可以看到大量条目形如{ A gift, just for you: , Confirm email address: , Hi {firstName},: }即左侧 key 是英文原文右侧 value 留空。运行时 i18next 被配置为returnEmptyString: false见 lib/i18n-core.js遇到空字符串会回退返回 key 本身于是英文语境下天然显示原文。好处是不需要维护一份完整的英文翻译文件坏处是——任何拼写调整都意味着 key 变更因此改动源文案必须重新跑提取脚本见下文提取字符串让所有 locale 文件同步改名。编写可翻译文案的硬性规则使用t()前请确认你所在的应用/服务中该助手是如何建立的。例如 Portal 通过import i18nLib from tryghost/i18n/registry/portal获得翻译工厂见 apps/portal/src/utils/i18n.js而 Ghost Core 服务端则同步require该包并在相应上下文注入t。遵循以下规则可以让后续所有 locale 的翻译质量与 CI 校验都保持在可控范围。1. 完整句子放进一次调用翻译不是逐词替换而是整句重排语序。必须把一条完整消息放进一次t()调用// Do整句作为一个 key t(Could not sign in. Please try again.) // Do not把一条消息拆进多个翻译调用译者在目标语言里将无法自由调整语序 t(Could not sign in.) t(Please try again.)同理也不要用已翻译片段拼句子——例如用t(Welcome) t(back)拼接欢迎语一旦目标语言的词序不同结果就会错乱。2. 动态值用命名变量named variables需要插入动态内容时在字符串内使用{变量名}占位并在第二个参数中传值t(Welcome back, {name}!, {name: member.name})注意这里的花括号{}是运行期的插值约定i18next 实例在初始化时被显式设置为interpolation.prefix: {、interpolation.suffix: }见 lib/i18n-core.js所以不要使用 i18next 默认的{{双花括号}}写法。3. 消息中含链接/按钮时用 doist/react-interpolate若一段文案中嵌入了链接、按钮等元素仍要把完整句子放在一个字符串中并用a之类的占位标签标记元素位置再用doist/react-interpolate的Interpolate组件把标签映射为真实 React 元素import Interpolate from doist/react-interpolate; Interpolate mapping{{a: a href{helpUrl} /}} string{t(Having trouble? aRead the help guide/a.)} /这样译者既可以整体翻译句子也可以调整标签前后的措辞而你依然能注入正确的 URL 属性。4. 保留{变量}与标签的名称——它们是跨语言校验的运行时契约所有语言文件中的{变量名}与标签名必须与英文 key 完全一致它们构成了运行时契约单词前后顺序允许不同语言重排但名称与拼写不得改动。英文模板中未使用的变量、以及翻译里出现的未知变量都会在下文的lint:translations中被当作错误拦截对应规则分别叫ghost/i18n/no-unused-variables与ghost/i18n/no-undefined-variables见 test/i18n.lint.js。翻译文件里的正确形态例如{ Welcome back, {name}!: Bon retour, {name} ! }提取字符串让源改动驱动所有 locale 同步当你在代码中新增或修改了任何源字符串之后必须从仓库根目录执行提取命令pnpm --filter tryghost/i18n translate这一条命令会依次执行五个命名空间的提取translate:ghost、translate:portal、translate:signup-form、translate:comments、translate:search最后运行generate-context.js更新所有 locale 文件i18next-parser 按命名空间扫描源码把新增/改名后的 key 写入每一个locales/locale/namespace.json。解析器配置见 i18next-parser.config.jskeySeparator: false与namespaceSeparator: false保证含.、:、空格的完整句子也能作为键output: locales/$LOCALE/$NAMESPACE.json决定产物位置sort: true让 key 按字母序排列保持 diff 干净。同步context.json生成脚本 generate-context.js 会遍历locales/en/下所有英文 key合并进 locales/context.json 并排序写回。为新 key 补充 context 描述context.json的作用是给每个 key 一句给译者看的说明——告诉译者这段文案出现在哪里、想传达什么。每新增一个 key你都应该顺手在context.json中为它补充描述例如{ Complete signup for {siteTitle}!: Shown as the subject line in the member signup confirmation email, where {siteTitle} is the site name. }这部分并非可选CI 环境下若存在空描述generate-context.js 会列出所有空描述 key 并以非零码退出若context.json与 locales 不一致也会直接报错提示先运行pnpm translate。因此请把源码改动、生成的 locale 改动、context.json 改动一起提交——它们属于同一次文案变更的三个产物拆开提交会导致 CI 或审阅失败。提交前自检翻译 lint 与包测试从仓库根目录运行pnpm --filter tryghost/i18n lint:translations pnpm --filter tryghost/i18n testlint:translations实现于 test/i18n.lint.js会遍历locales/*/*.json逐个校验key-翻译值对。它实际是一套自定义 ESLint 风格检查器核心包括ghost/i18n/no-invalid-translations变量括号语法错误如未闭合的{、多余的}ghost/i18n/no-unused-variableskey 中定义了变量但翻译值没有使用ghost/i18n/no-undefined-variables翻译值里出现了 key 未定义的未知变量ghost/i18n/no-unused-ignoresignore 文件中test/i18n-ignores.json存在已经不再触发的豁免记录提醒清理。由于 JSON 本身不支持注释确需豁免个别误报时使用i18n-ignores.json而不是修改检查器。包测试 test/i18n.test.js 还承担两件重要的事其一pnpm test本身会以pnpm test:base pnpm translate的形式再次运行提取因此跑完测试后应复查产生的 diff 并提交任何预期内的生成改动其二它验证真实的翻译行为——例如荷兰语下t(Name)返回Naam、挪威语变体no会按no → nb → en的回退链落到书面挪威语见 lib/i18n-core.js 的fallbackLng配置。运行期原理一份配置两条加载路径理解了配置与校验再来看运行期实现会让整条链路更清晰。核心逻辑集中在 CJS/ESM 一对镜像孪生文件中lib/i18n-core.js —— 供 Ghost Core 以require()同步加载的 Node 路径lib/i18n-core.mjs —— 纯 ESM 的浏览器端孪生实现。之所以要维护两份是因为浏览器打包产物里若混入require(...)/module.exports会在加载时直接抛错而服务端需要同步 require。两者必须行为一致为此测试专门写了CJS/ESM core parity分组来防止两者漂移见 test/i18n.test.js。createI18n实例化 i18next 时的关键配置见 lib/i18n-core.js包括语言与命名空间缺省值lng: en、ns: portalkeySeparator: false/nsSeparator: false允许含.、:的整句作 keyreturnEmptyString: false空翻译值回退为 key英文原文fallbackLngno → [nb, en]其余默认[en]插值符{/}且portal与theme命名空间关闭 HTML 转义escapeValue: false。浏览器端不直接引入整包而是通过每个命名空间独立的静态入口按需加载。以 lib/registry/ghost.mjs 为例它用import.meta.glob打包../../locales/*/ghost.json形成注册表再由共享工厂 lib/esm-factory.mjs 装配成与 CJS 端调用签名一致的工厂函数。这样打包器只会把该 app 真正用到的那个命名空间的 locale 文件打进去未打包的未知 locale 会自动回退英文。对开发者而言日常只需记住Ghost Core 走lib/i18n.js的同步路径public apps 走tryghost/i18n/registry/namespace的按命名空间入口。流程小结一次合规的文案变更在源码中通过对应 app/service 的t()写入整句英文 key动态值用{name}内嵌元素用a标签配合Interpolate从仓库根目录运行pnpm --filter tryghost/i18n translate自动更新locales/locale/namespace.json与 locales/context.json在context.json中为每个新 key 补上供译者阅读的描述否则 CI 会拒绝运行pnpm --filter tryghost/i18n lint:translations与pnpm --filter tryghost/i18n test本地校验复查测试触发的提取 diff将源码改动、生成的 locale 改动与context.json改动一起提交并开启 PR需要新增语言或补全某语言翻译的译者流程则遵循 Translating Ghost 中关于 locale 注册与翻译文件编辑的说明。【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考