Skip to content

Commit 708595d

Browse files
committed
docs(spec): regenerate the email-template reference page and add the changeset
`gen:docs` projects `EmailTemplateDefinitionSchema.locale`'s describe into content/docs/references/system/email-template.mdx, so the source correction without this regeneration would have shipped the old sentence to every reader of the reference page. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent c50c54e commit 708595d

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
docs(spec): scope the email-template locale-floor claims to a call that NAMES a locale (#18056)
6+
7+
Clause-②: yes — no accept set moves (no key is added, removed or revalidated),
8+
but what a PUBLISHED package states about its own resolution contract is
9+
corrected, which is a contract act in substance.
10+
11+
`packages/spec` stated two different rung counts for one resolution.
12+
`EmailTemplateDefinitionSchema.locale`'s `describe` and the
13+
`EMAIL_TEMPLATE_FLOOR_LOCALE` docblock published **one** retry rung and an
14+
explicit "no fallback floor at all"; `SendTemplateInput.locale` in
15+
`contracts/email-service.ts`, same package, documents a **three-rung** ladder
16+
whose third rung is reachable exactly on the path the first says cannot exist.
17+
18+
Measured against the runtime rather than reconciled by preference —
19+
`EmailService.resolveAndRenderTemplate` and `createSysEmailTemplateLoader` in
20+
`@objectstack/plugin-email`, and the CI pins in
21+
`template-locale-resolution.test.ts` — the three-rung text is the correct one:
22+
23+
1. the named locale, matched exactly (no language-subtag folding);
24+
2. the literal `en-US`, which is also where a call naming no locale starts;
25+
3. **only for a call that named no locale**, and only when the bundle carries
26+
no `en-US` row: the bundle's lowest locale tag.
27+
28+
So a bundle with no `en-US` row dead-letters (`TEMPLATE_NOT_FOUND`, permanent)
29+
for every recipient whose locale was NAMED, and silently renders whichever
30+
language sorts first for every call that named none. The shipped declaration
31+
promised the loud permanent refusal on the path where the runtime performs the
32+
silent fill; an author reading it was told a missing locale always
33+
dead-letters. Both call shapes are now named wherever the floor is claimed, and
34+
the ladder itself is stated in one place only.
35+
36+
Also corrected: `SendTemplateInput.template` said the service "picks the
37+
best-matching locale row", which the resolver has never done — there is no
38+
best match and no folding, only the ladder above.
39+
40+
`defineStack`'s `warnEmailTemplateLocaleFloor` gains a declaration of the two
41+
shapes it deliberately does NOT examine (a stack whose `i18n.supportedLocales`
42+
is absent or empty; a bundle whose tags all fall outside `supportedLocales`) —
43+
both can still ship a floorless bundle. Its logic is unchanged and its warning
44+
stays advisory; whether either shape should warn is the ADR-0049
45+
enforce-or-remove question and is not answered here. Both shapes are now pinned
46+
against a warning discriminator so neither can change without a test saying so.

‎content/docs/references/system/email-template.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const result = EmailTemplateDefinitionSchema.parse(data);
4444
| **name** | `string` | ✅ | Template identifier (dotted snake_case) |
4545
| **label** | `string` | ✅ | Display label |
4646
| **category** | `Enum<'auth' \| 'notification' \| 'workflow' \| 'marketing' \| 'custom'>` | optional (default: `"custom"`) | |
47-
| **locale** | `string` | optional (default: `"en-US"`) | BCP-47 locale (e.g. en-US, zh-CN) — the bundle key the resolver matches EXACTLY, with one retry rung: the literal `en-US`. No language-subtag folding, so `en` and `en-US` are different bundles and neither reaches the other. A bundle with no `en-US` row therefore has no fallback floor: any recipient locale it does not carry a row for raises TEMPLATE_NOT_FOUND, which is permanent — the delivery dead-letters with no retry. Your stack's own `i18n.defaultLocale` is the wrong tag here unless it is spelled `en-US`. |
47+
| **locale** | `string` | optional (default: `"en-US"`) | BCP-47 locale (e.g. en-US, zh-CN) — the bundle key the resolver matches EXACTLY. A call that NAMES a locale gets exactly one retry rung, the literal `en-US`, with no language-subtag folding: `en` and `en-US` are different ROWS of one bundle and neither reaches the other, so a bundle with no `en-US` row has no fallback floor for those calls and every recipient locale it does not carry a row for raises TEMPLATE_NOT_FOUND, which is permanent — the delivery dead-letters with no retry. A call naming NO locale is the other case and does not dead-letter: it drops to the bundle's lowest locale tag and renders that silently. Your stack's own `i18n.defaultLocale` is the wrong tag here unless it is spelled `en-US`. |
4848
| **subject** | `string` | ✅ | Subject template |
4949
| **bodyHtml** | `string` | ✅ | HTML body template |
5050
| **bodyText** | `string` | optional | Plain-text body template (auto-derived from HTML when omitted) |

0 commit comments

Comments
 (0)