ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Webiny 网站构建器多语言支持:translatePage Mutation 设计与实现解析

Webiny 网站构建器多语言支持:translatePage Mutation 设计与实现解析 CMS后端前端【免费下载链接】webiny-jsOpen-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.项目地址https://gitcode.com/gh_mirrors/we/webiny-js点击查看免费下载导读Webiny 的开源仓库为 Website Builder 引入了translatePageGraphQL Mutation用于在 CMS 中创建页面的语言版本它将现有页面完整复制到指定文件夹、为复制页写入语言代码、以/languageCode前缀改写 URL 路径并通过properties.sourcePage建立基础页 → 翻译页的可追溯血缘。本文以 ai-context/prds/translate-page.md 为核心结合packages/api-website-builder与packages/languages的源码与测试完整讲解该 Mutation 的需求背景、GraphQL 接口、TranslatePageUseCase执行流程、DuplicatePageRepository回调优化、PagePath路径规则、语言查询用例以及测试策略读完即可理解如何在 Webiny 中实现并验证页面的多语言复制。一、问题背景为什么需要translatePageWebiny 的 Website Builder 最初没有为页面创建语言版本的能力。在运营多语言站点的内容编辑者只能手工复制页面、再手动调整路径和元数据手工复制容易遗漏内容或出错复制页与基础页之间没有任何可追踪的关联系统无法回答哪些页面是哪一页的翻译这类结构性问题。translatePage(pageId, languageCode, folderId)Mutation 正是为此设计的它一次性完成完整复制页面 → 放入指定文件夹 → 分配语言代码 → 改写 URL 路径前缀 → 记录来源页面引用的整套动作从而在基础页与所有翻译页之间建立清晰的单向前置血缘lineage。翻译过程仍由人工触发并继续在 UI 中编辑内容Mutation 只提供结构脚手架自动化/AI 翻译不在本次范围内。二、GraphQL 接口Mutation 定义与 Resolver 模式2.1 Schema 定义Mutation 挂在已有的WbMutation类型上见 packages/api-website-builder/src/graphql/pages/pages.typeDefs.tstranslatePage(pageId: ID!, languageCode: String!, folderId: ID!): WbPageResponse三个参数的含义参数类型说明pageIdID!待翻译的源页面 ID可以是基础页也可以是已翻译页languageCodeString!目标语言代码如de、fr必须存在于语言注册表中folderIdID!翻译页要放入的目标文件夹 ID便于按项目目录结构组织翻译内容2.2 Resolver 实现Resolver 与其它页面 Mutation 完全同构见 packages/api-website-builder/src/graphql/pages/pages.gql.ts声明依赖TranslatePageUseCase由context.container解析调用translatePage.execute({ pageId, languageCode, folderId })若result.isFail()抛出带error.message的异常成功则返回result.value新的WbPage。builder.addResolver({ path: WbMutation.translatePage, dependencies: [TranslatePageUseCase], resolver(translatePage) { return ({ args, context }) resolve(async () { ensureAuthentication(context); const result await translatePage.execute({ pageId: args.pageId, languageCode: args.languageCode, folderId: args.folderId }); if (result.isFail()) { throw new Error(result.error.message); } return result.value; }); } });三、前置依赖webiny/languages的语言查询用例translatePage校验语言代码的前提是webiny/languages包先提供公开的查询用例。此前该包没有专门的查询用例内部依赖 HeadlessCMS 的通用用例PRD 要求新增两个用例二者在仓库中均已实现GetLanguageByCodeUseCase输入语言代码字符串返回语言条目或失败结果用于校验语言代码是否存在。实现见 packages/languages/src/api/features/GetLanguageByCode/GetLanguageByCodeUseCase.tsListLanguagesUseCase列出全部语言条目供 UI 及其它消费方使用实现见 packages/languages/src/api/features/ListLanguages/ListLanguagesUseCase.ts。两个用例都遵循代码库统一的createAbstraction/createImplementation抽象模式抽象层定义接口实现层通过构造函数注入仓库依赖。例如GetLanguageByCodeUseCase的抽象abstractions.ts包含GetLanguageByCodeRepository与GetLanguageByCodeUseCase两层抽象命名空间内导出Interface/Return/Error类型实现层只做转发class GetLanguageByCodeUseCaseImpl implements UseCaseAbstraction.Interface { constructor(private repository: GetLanguageByCodeRepository.Interface) {} async execute(code: string): UseCaseAbstraction.Return { return this.repository.execute(code); } } export const GetLanguageByCodeUseCase createImplementation({ abstraction: UseCaseAbstraction, implementation: GetLanguageByCodeUseCaseImpl, dependencies: [GetLanguageByCodeRepository] });PRD 中提到这两个用例通过使用 CMSListLatestEntriesUseCase并以code字段过滤的仓库实现仓库底层数据模型是LANGUAGE_MODEL_ID即wbyLanguage。语言模型包含字段name名称、code代码、directionltr/rtl、isDefault是否默认语言、enabled是否启用——测试中创建语言条目即写入这组字段见下文测试章节。两个用例均在 languages 包的 API Extension 中注册并从包的公开 APIpackages/languages/src/exports/api/languages.ts导出供api-website-builder跨包引用。四、TranslatePageUseCase执行流程与源码剖析TranslatePageUseCase位于 packages/api-website-builder/src/features/pages/TranslatePage/TranslatePageUseCase.ts其抽象定义见 abstractions.ts。抽象层声明的参数类型为export interface ITranslateWbPageParams { pageId: string; languageCode: string; folderId: string; } export interface ITranslatePageUseCase { execute(params: ITranslateWbPageParams): PromiseResultWbPage, UseCaseError; }实现类TranslatePageUseCaseImpl通过构造函数注入五个依赖依赖来源职责WbPermissionsapi-website-builder 内部写入权限校验GetPageByIdUseCaseapi-website-builder 内部读取源页以解析血缘GetLanguageByCodeUseCasewebiny/languages校验语言代码存在性ListLanguagesUseCasewebiny/languages获取全部受支持语言代码DuplicatePageRepositoryapi-website-builder 内部执行复制并应用翻译修改执行步骤与 PRD 一一对应且源码做了两处增强权限校验调用permissions.canCreate(page)无权限返回PageNotAuthorizedError校验语言代码调用getLanguageByCode.execute(params.languageCode)失败则返回PageTranslationError(languageCode)获取支持的语言代码集调用listLanguages.execute()得到supportedCodes源码新增步骤供路径改写使用失败返回PagePersistenceError读取源页调用getPageById.execute(params.pageId)用于解析血缘与原始路径解析properties.sourcePagesourcePage.properties.sourcePage ?? sourcePage.entryId—— 若源页本身就是翻译页已有sourcePage沿用其值否则使用源页自身的entryId。这保证了血缘永远指向根基础页调用DuplicatePageRepository.execute并传入回调在回调中改写复制页数据语言、血缘、路径、文件夹返回结果成功则返回Result.ok(复制后的新 WbPage)。// 关键流程节选 const languageResult await this.getLanguageByCode.execute(params.languageCode); if (languageResult.isFail()) { return Result.fail(new PageTranslationError(params.languageCode)); } // ... const resolvedSourcePageId sourcePage.properties.sourcePage ?? sourcePage.entryId; const result await this.duplicatePageRepository.execute( { id: params.pageId }, ({ duplicate }) { const originalPath: string sourcePage.properties.path ?? /; const pagePath PagePath.create(originalPath); const translatedPath pagePath .setLanguageCode(params.languageCode, supportedCodes) .toString(); duplicate.properties.language params.languageCode; duplicate.properties.sourcePage resolvedSourcePageId; duplicate.properties.path translatedPath; duplicate.properties.title sourcePage.properties.title; duplicate.location.folderId params.folderId; } );注意 PRD 强调委托给 Repository 而非 UseCase以绕开重复的权限校验和事件——源码中DuplicatePageUseCase与TranslatePageUseCase都共享底层DuplicatePageRepository而翻译场景直接走仓库层确实避免了对同一操作执行两次权限检查并且不发布任何事件PRD 明确暂不引入PageBeforeTranslateEvent/PageAfterTranslateEvent留待后续迭代。4.1 Feature 注册新的TranslatePageFeature遵循与CreatePageFeature、DuplicatePageFeature相同的注册模式见 feature.tsexport const TranslatePageFeature createFeature({ name: WebsiteBuilder/TranslatePage, register(container) { container.register(TranslatePageUseCase); } });PRD 要求TranslatePageRepository以单例作用域注册、TranslatePageUseCase正常注册——在当前的createFeature/createImplementation抽象模式下TranslatePageUseCase经由 DI 容器解析并自动注入其依赖链。五、性能优化DuplicatePageRepository回调机制5.1 为什么需要回调翻译需要同时改写语言、血缘、路径、文件夹四个字段。若先复制、再单独发一次更新请求就会多一次往返round-trip。因此DuplicatePageRepository被增强为接受一个可选的callback参数在 CMSCreateEntryUseCase调用之前让调用方就地修改页面数据一次写入完成复制与改写。5.2 抽象与实现抽象层packages/api-website-builder/src/features/pages/DuplicatePage/abstractions.ts定义了回调类型export type DuplicatePageData Pick WbPage, bindings | elements | location | properties | metadata | extensions ; export interface DuplicatePageCallbackParams { original: WbPage; duplicate: DuplicatePageData; } export type DuplicatePageCallback (params: DuplicatePageCallbackParams) Promisevoid | void; export interface IDuplicatePageRepository { execute( params: IDuplicateWbPageParams, callback?: DuplicatePageCallback ): PromiseResultWbPage, RepositoryError; }实现层DuplicatePageRepository.ts的执行逻辑通过pageModelProvider.get()获取页面模型getEntryById.execute(pageModel, params.id)读取源条目按错误码区分PageNotFoundErrorCms/Entry/NotFound与PagePersistenceError用EntryToPageMapper.toPage映射为WbPage用lodash.pick只挑选复制需要的字段bindings、elements、location、properties、metadata、extensions——这正是 PRD 用户故事 7 要求的完整副本元素、绑定、元数据、扩展构造newPageData默认行为是为路径追加-copy后缀、标题加Copy of 前缀若传入了 callback在createEntry之前执行调用方可以覆盖这些默认值翻译场景即把路径从/about-copy改写为/de/about调用 CMSCreateEntryUseCase创建条目校验失败映射为PageValidationError其余映射为PagePersistenceError再次经EntryToPageMapper映射后返回新WbPage。// 回调注入点节选 const newPageData { ...dataToDuplicate, properties: { ...dataToDuplicate.properties, path: ${originalPage.properties.path}-copy, title: Copy of originalPage.properties.title } }; if (callback) { await callback({ original: originalPage, duplicate: newPageData }); } const result await this.createEntry.execute(pageModel, { location: newPageData.location, values: newPageData });由于callback是可选的现有DuplicatePageUseCase等未传回调的调用方行为完全不变这是一次向后兼容的增强。六、路径改写规则PagePath.setLanguageCode路径前缀逻辑被封装为领域值对象 PagePath.tssetLanguageCode(code, supportedCodes)的规则非法代码防护code不在supportedCodes中直接抛错UseCase 层已用GetLanguageByCodeUseCase提前校验归一化路径不以/开头则补上/根路径特例路径为/时结果取/code如/de不产生/de/尾斜杠——对应 PRD 用户故事 8已有语言前缀若首段已是受支持的语言代码说明源页本身是翻译页则替换首段而不是再叠加保证翻译链路上路径不出现/de/fr/...的嵌套无语言前缀在路径前直接插入/code。setLanguageCode(code: string, supportedCodes: string[]): PagePath { if (!supportedCodes.includes(code)) { throw new Error(Language code ${code} is not in supported codes); } const normalized this.path.startsWith(/) ? this.path : /${this.path}; if (normalized /) { return new PagePath(/${code}); } const pathSegments normalized.split(/).filter(Boolean); const firstSegment pathSegments[0]; if (supportedCodes.includes(firstSegment)) { pathSegments[0] code; return new PagePath(/${pathSegments.join(/)}); } return new PagePath(/${code}${normalized}); }路径演进的完整示例场景源路径目标语言结果路径基础页翻译/aboutde/de/about根路径首页翻译/de/de已翻译页再翻译de → fr/de/aboutfr/fr/about已翻译页翻译为同前缀语言/de/aboutde/de/about首段替换为同值七、页面属性契约与领域错误翻译页在properties上新增三个语义明确的字段属性类型语义properties.languagestring \| undefinedundefined或缺失表示默认语言翻译页为语言代码字符串如de、frproperties.sourcePagestring \| undefined来源基础页的entryId基础页为undefined始终指向根基础页绝不指向中间翻译页properties.pathstring翻译页带/languageCode前缀基础页保留原始路径领域错误PageTranslationError定义在 packages/api-website-builder/src/domain/page/errors.ts遵循项目的BaseError模式携带结构化错误码与数据export class PageTranslationError extends BaseError{ languageCode: string } { override readonly code WebsiteBuilder/Page/TranslationError as const; constructor(languageCode: string) { super({ message: Language ${languageCode} was not found., data: { languageCode } }); } }八、跨包依赖api-website-builder从webiny/languages/exports/api/languages.js导入GetLanguageByCodeUseCase与ListLanguagesUseCase并从 DI 容器解析。这是两个包之间新增的依赖关系需同步反映在api-website-builder的package.json与依赖图中PRD 在 Further Notes 中明确要求。这也意味着引入translatePage时必须确认webiny/languages已被部署且语言条目已初始化。九、测试策略以公开接口验证外部行为PRD 的测试原则是通过 UseCase 的execute公开接口验证外部行为断言返回页面的结构与内容而不是内部仓库调用细节。仓库中的测试文件为 packages/api-website-builder/tests/translatePage.test.ts共 5 个用例覆盖 PRD 列出的全部场景翻译基础页should translate a base page创建语言de→ 创建pageA→translatePage执行 → 断言properties.language de、properties.sourcePage page.entryId、properties.path /de/page-a、location.folderId de-folder、新页面id/entryId均不同于源页翻译已翻译页并解析血缘should translate an already-translated page and resolve lineage to the root base page基础页 → 德语 → 法语三级链条断言法语页的sourcePage仍指向最初的基础页entryId路径为/fr/page-a验证路径首段替换而非叠加根路径处理should handle root path / correctly源路径/翻译后断言为/de非法语言代码报错should return an error for an invalid language code传入xx断言result.isFail()且error.code WebsiteBuilder/Page/TranslationError完整副本可回读should produce a full copy that can be fetched by ID翻译后用GetPageByIdUseCase按新 ID 取回断言语言、血缘、路径、文件夹均正确。测试通过useHandler构建上下文并从context.container解析CreatePageUseCase、TranslatePageUseCase、GetPageByIdUseCase等用例语言条目则直接写入wbyLanguage模型name、code、direction: ltr、isDefault: false、enabled: true。这与 PRD 提到的先例——packages/api-website-builder/tests/pages.test.ts 中从容器解析用例并对Result值断言——保持同一风格。webiny/languages侧的GetLanguageByCodeUseCase也有对应测试见 packages/languages/tests/languages.test.ts取回已有语言断言正确返回取不存在语言代码断言失败结果。十、范围边界与后续演进PRD 明确列出本次实现不包含的内容理解这些边界有助于避免误解自动/AI 翻译页面内容仅结构脚手架翻译由人工在 UI 中完成触发翻译的 UI 改动本功能仅 API 层同一文件夹内语言唯一性约束允许重复翻译页的发布行为沿用现有发布流程批量翻译修改基础页以追踪其翻译列表血缘是单向的翻译页指向基础页PageBeforeTranslateEvent/PageAfterTranslateEvent推迟到后续迭代为已有基础页回填properties.language默认语言保持隐式undefined。TranslatePageUseCase被刻意设计为干净的抽象abstractions.ts依赖全部通过构造函数注入未来无论是替换底层实现还是接入自动化翻译服务都只需替换/扩展注册的实现而不影响调用方——这正是可替换的抽象这一设计目标的具体落地。参考源码索引PRD 原文ai-context/prds/translate-page.mdMutation schemapackages/api-website-builder/src/graphql/pages/pages.typeDefs.ts#L253Resolverpackages/api-website-builder/src/graphql/pages/pages.gql.ts#L280-L300UseCase 实现packages/api-website-builder/src/features/pages/TranslatePage/TranslatePageUseCase.tsUseCase 抽象packages/api-website-builder/src/features/pages/TranslatePage/abstractions.tsFeature 注册packages/api-website-builder/src/features/pages/TranslatePage/feature.ts复制仓库抽象与回调类型packages/api-website-builder/src/features/pages/DuplicatePage/abstractions.ts复制仓库实现packages/api-website-builder/src/features/pages/DuplicatePage/DuplicatePageRepository.ts路径规则packages/api-website-builder/src/domain/page/PagePath.ts领域错误packages/api-website-builder/src/domain/page/errors.ts#L45-L54语言查询用例packages/languages/src/api/features/GetLanguageByCode/GetLanguageByCodeUseCase.ts、packages/languages/src/api/features/ListLanguages/ListLanguagesUseCase.tslanguages 公开 API 导出packages/languages/src/exports/api/languages.ts测试packages/api-website-builder/tests/translatePage.test.ts、packages/languages/tests/languages.test.ts赞分享CMS后端前端【免费下载链接】webiny-jsOpen-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.项目地址https://gitcode.com/gh_mirrors/we/webiny-js点击查看免费下载相关推荐Webiny 多语言页面翻译实现指南translatePage Mutation 与 TranslatePageUseCase 深度解析Webiny 多语言页面翻译实现指南translatePage Mutation 与 TranslatePageUseCase 深度解析 本文基于 WebinCMS后端前端EmuDeck终极指南Steam Deck一键配置30游戏模拟器EmuDeck终极指南Steam Deck一键配置30游戏模拟器 想要在Steam Deck上轻松玩转经典游戏吗EmuDeck是您的完美解决方案这款专为CLI开发工具Mapbox Android Demo位置服务详解实时定位与用户位置跟踪实现指南Mapbox Android Demo位置服务详解实时定位与用户位置跟踪实现指南 Mapbox Android Demo是一款展示Mapbox地图SDK功能的上一篇8个Obsidian美化技巧让你的笔记工具更专业高效下一篇3步飞升智能音箱改造MiGPT让小爱秒变本地化AI助手创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表