Repository navigation
fix(plugin-email): a templated email to a named locale folds it against the template's locales and tries the stack's declared fallback before en-US - #28917
Conversation
…d try the declared fallback before en-US The built-in registry loader exposes its bundle's sorted locale tags under a module-private key; the service folds the named locale and then the stack's declared fallback with resolveBundleLocale before the en-US rung, loading each resolved tag exactly. EmailServicePlugin reads the fallback from the i18n service at each send. A no-locale call and a replacement loader keep their ladders. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 233f9292a17816263d5f4b71f4cae662e815181e && git checkout 233f9292a17816263d5f4b71f4cae662e815181e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9a9823d921a85a523529c25978daeb8229fa4ff9 2c76b75611703a5c0c1aba88b98fbe40ef644596 && git checkout -B drift-repro 9a9823d921a85a523529c25978daeb8229fa4ff9 && git merge --no-ff 2c76b75611703a5c0c1aba88b98fbe40ef644596
node scripts/docs-audit/affected-docs.mjs --json 9a9823d921a85a523529c25978daeb8229fa4ff9
|
|
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」. 席位意见:与 diff 相符。指定语言的模板邮件现在先按平台统一的 BCP-47 规则匹配模板已有的语言(en-GB→en、zh-TW→zh-CN),再试站点声明的回退语言,然后才是 en-US;未指定语言的调用和自定义加载器照旧。
|
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Requested by Leehom · project thread
Fixes #28880
Clause-②: no
Metadata-protocol: none
Skills-docs: none — no file under
skills/**teaches the email template locale ladder.维护者速读
en-US,所以en-GB收件人拿不到en行,zh-TW拿不到zh-CN行,栈声明的i18n.fallbackLocale也从不读取,这些投递都以TEMPLATE_NOT_FOUND死信。resolveBundleLocale:精确、大小写、基础语言、同语言变体,变体取升序第一个)折叠到模板已有的 locale;再按同样规则试声明的 fallback;再en-US;最后仍是永久失败的TEMPLATE_NOT_FOUND。en-US,再取最小 tag),不读声明的 fallback;替换的TemplateLoader保持原来的精确规则。折叠是内置 loader 的能力,通过模块私有的 symbol 提供,不导出、不加在TemplateLoader上,公开 d.ts 没有变化。SendTemplateInput和email-template.zod.ts的说明文字因此过时,已由 spec: the email template locale docs say the resolver folds no language subtag and retries only en-US, which #28880 makes wrong #28896 跟进;本 PR 不改packages/spec/**。What changes
All in
packages/plugins/plugin-email/src/.template-loader.ts:createRegistryEmailTemplateLoader's loader also answers, under a module-private symbol (TEMPLATE_BUNDLE_LOCALES, declared inemail-service.ts, not re-exported from the package index and not a member ofTemplateLoader), the locale tags the name's bundle carries, de-duplicated and sorted ascending. Itsloadis unchanged and stays exact.email-service.ts(resolveAndRenderTemplate): for a call that names a locale over a loader carrying that symbol, it folds the named tag withresolveBundleLocale(imported from@objectstack/spec/system, no third copy of the ladder), then the declared fallback the same way, loading each resolved tag exactly and skipping a tag already tried; then the existingen-USrung. A loader without the symbol, and every no-locale call, take exactly the base path.EmailService.setTemplateFallbackLocale(read)stores the fallback reader (the class is not exported, so this is not a published member). TheTEMPLATE_NOT_FOUND/TEMPLATE_INACTIVEmessages, the variables check and the envelope columns are untouched.email-plugin.ts: the composition point.EmailServicePlugin.inithands the service a reader that resolves thei18nservice at each send and answers itsgetFallbackLocale()(i18n.fallbackLocale, elsedefaultLocale); a host with no i18n service declares none.The fold is a built-in-loader capability: a replacement
TemplateLoaderkeeps today's exact ladder, declared fallback included (pinned).Tests
template-locale-fold.test.ts(13, the real service over the real registry loader):en-GB→en;zh-TW→zh-CN;zh→zh-CN;zh-cn→zh-CN; variant expansion takes the first base-language match in ascending tag order in both list orders (zh-MO→zh-CN); an exact hit wins over every fold;fallbackLocale: 'zh-CN'with no match →zh-CNbeforeen-US; the fallback is folded (zh→zh-CN); a fallback with no row →en-US; no fallback →en-US; nothing at all →TEMPLATE_NOT_FOUND: welcome …; a no-locale call answers as today and never reads the fallback; a loader without the symbol answers exactly as on base, with a declared fallback (['en-GB', 'en-US'], thenTEMPLATE_NOT_FOUND).template-locale-fold.integration.test.ts(2, bootedObjectKernelwithObjectQLPlugin, an app registering the duly shapeen+zh-CNbundle through themanifestservice, and the realEmailServicePluginwith its built-in loader):en-GBsends theenrow andzh-TWthezh-CNrow; with an i18n service declaringzh-CN, ajarecipient sends thezh-CNrow.origin/mainb15b634c): booted send, base throwsTEMPLATE_NOT_FOUND: lf_lead_time (locale=en-GB)and(locale=ja), head sends[en],[zh-CN],[zh-CN]. Unit file on base (with only a no-opsetTemplateFallbackLocaleshimmed in so the file loads): 7 red, the 6 guards green.en-US@objectstack/plugin-emailsuite: 47 files, 764/764, including the email-template-render (b): sendTemplate with no locale renders an arbitrary locale instead of the en-US default #7731 file that pins the loader's own exactloadand the service ladder over an exact loader, unchanged.Acceptance notes
TemplateLoader,EmailServiceOptionsandcreateRegistryEmailTemplateLoader's signature are as before; the builtdist/index.d.tsis identical to base.SendTemplateInput.localedoc says a call that omits the locale starts aten-US, so the declared fallback stays out of that ladder.SendTemplateInput.template/.locale(packages/spec/src/contracts/email-service.ts) say "no language-subtag folding", andemail-template.zod.tssaysen-USis the only rung retried. Follow-up spec: the email template locale docs say the resolver folds no language subtag and retries only en-US, which #28880 makes wrong #28896 (domain:spec); not touched here.NotificationTemplateStore's own ladder (named, primary subtag,en) inservice-messagingis unchanged (out of scope).Verification
Gates:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 60 commands on the diff; all 60 exit 0, and--ranreconciles 60 derived, 60 run, 0 NOT-MEASURED.pnpm --filter @objectstack/spec check:error-code-provenance: exit 0.pnpm --filter @objectstack/plugin-email typecheck(runscheck:test-typecheck): first exit 2 (TS2353, the symbol key on the loader's object literal typedTemplateLoader), fixed by typing the local asTemplateLoader & { [key]: … }, then exit 0. Scoped eslint over the five changed.tsfiles: exit 0.pnpm buildof the package: the emitteddist/index.d.tsmatches base. After the final merge ofmain(9a9823d9, nothing underpackages/plugins/plugin-emailorpackages/spec): the threetemplate-locale*files green (38 tests).The booted send in
template-locale-fold.integration.test.tsis the order's realsendTemplatethrough the email service with the built-in loader; the duly app itself was not run. Full dogfood not owed (no!, no narrowing arm).Generated by Claude Code