Repository navigation
feat(flows): notify nodes speak the recipient's own language - #1949
Merged
Merged
Conversation
Every `notify` node across sales, service and revenue stops carrying an
inline English `title`/`message` and references a `sys_email_template`
bundle instead, supplying its record values through `templateData`.
The delivery path resolves `(name, locale)` per recipient AFTER fan-out,
off that person's own `sys_user.locale`, so one notification addressed to
two people with different languages arrives in two languages.
19 templates x 4 locales = 76 rows, split by OWNER DOMAIN into
`src/{sales,revenue,service}/email-templates/` — 14 / 3 / 2. Measured
first: no template is referenced from more than one package, on four
independent axes, so the split needs no shared directory.
Wording is unchanged throughout: every string is the text its node
already sent, with the flow's `{var.path}` interpolations re-expressed as
the `{{key}}` holes the renderer fills from `templateData`.
Ten assertions that observed a notification through its inline `n.title`
are repointed at the `templateData` key carrying the same value; the
audience, severity and dot-walk guards beside them are untouched.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-elon-musk
marked this pull request as ready for review
September 16, 2026 09:36
This was referenced Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #1945 —⚠️ deliberately NOT a closing keyword.
Predecessor: PR #1893. That PR (another seat,
2026-09-11, no movement since) did thissame conversion against the pre-ADR-0130 tree, and its whole file surface went away with the
src/flows/relayout. This is a re-landing of that work, not a rewrite, and it is a new PR:⛔ nothing here drives, takes over, modifies or closes #1893.
What this does
Every
notifynode across sales, service and revenue stops carrying an inline Englishtitle/messageand references asys_email_templatebundle instead, supplying its recordvalues through
templateData. The delivery path resolves(name, locale)per recipient,after fan-out — the recipient's own
sys_user.locale, else the deployment default — so onenotification addressed to a Chinese-speaking rep and a Japanese-speaking manager arrives in two
languages.
Wording is unchanged. Every string is the text its node already sent, with the flow's
{var.path}interpolations re-expressed as the{{key}}holes the renderer fills from thatnode's
templateData. No notification says anything new.Result: zero cross-domain templates. Every one is one-to-one with a single owning package,
so the per-owner split the ruling prescribes needs no shared directory and no copy.
Method. The notify nodes were enumerated at RUNTIME, not by grep: the four package flow
barrels were imported and every node tree walked recursively, including
loopbodies and thetry/catchregions of theguarded()containment (six of the nineteen nodes sit insideone, and a flat file scan cannot see which flow owns them). Each node's content was then
grouped across packages on four independent axes, so a single normalisation choice cannot
manufacture the answer:
topicThe one axis that collapses (
topic, 18 not 19) islead_routing, carried by two nodes of thesame flow in the same package — so it does not span domains either, and those two get
distinct template names (
crm.lead_routing_hot/crm.lead_routing_new) because their copydiffers.
Ruling ③ — where the files landed
Maintainer ruling, quoted verbatim and untranslated (#1920, 2026-09-16):
So there is ⛔ no
src/email-templates/at the root. The 19 templates split by the owner domainof the flow that names them:
src/sales/email-templates/src/revenue/email-templates/src/service/email-templates/_bundle.ts— the one spelling of "a locale pack becomes rows" — lives insrc/sales/becausetwo other packages need it, which is the AGENTS.md import rule's own answer for a shared source
(lowest package all consumers depend on); service and revenue import it along their edge into
sales, exactly as
flows/_guarded-iteration.tsandobjects/_hook-api.tsare imported.Registration goes through
byExportName()inobjectstack.composition.ts, so reaching thepackage barrel IS reaching the registration — no hand-kept ordered array, because nothing about
the order is load-bearing here: a row is resolved by
(name, locale)at delivery, never byposition.
objectstack.config.tshands the result todefineStack({ emailTemplates }).en-US, noten, and that is the resolution ladder's doing.EmailService.resolveAndRenderTemplatematches(name, locale)exactly, then retries exactlyonce against its own
DEFAULT_TEMPLATE_LOCALE, the literalen-US. A recipient on this app'sdefault
entherefore misses the first probe and lands on the second. Tagging the rowseninstead would invert that:
en-USis a truthy preferred locale, so the third probe is skippedand the send fails
TEMPLATE_NOT_FOUND.Token ratchet — measured per package, on all twelve rows
node scripts/check-source-token-ratchet.mjs→ exit 0, twelve of twelve green. The newdirectories land in the residual layer (
other authored metadata), counted in eachpackage's authored total and in no headline layer — confirmed by the gate's own printed line.
src/salessrc/revenuesrc/service⭐ The conversion itself gives tokens back; the four-locale content is the whole cost. The
business semanticslayer (which is whereflows/is counted) went DOWN in all three packages— sales 54,285 → 54,179, revenue 15,196 → 15,169, service 12,646 → 12,593, −186 in total —
because 19 inline
title/messagepairs are longer than 19template+templateDatapairs.The gross cost of the new directories, through the gate's own
authoredText()+tokensOf():sales ~3,096 · revenue ~822 · service ~757.
⛔ No ceiling was raised. ⛔ No unrelated prose was compressed to buy room. ⛔ Nothing was filed
into an unmeasured directory to dodge the count: the content sits in
email-templates/, whichis measured, and
translationswas not used as a hiding place.The
test/**fence — exactly the 10 assertions it was opened forRuling ② opened #1579's fence for the assertions that observe a notification through its inline
n.title— an implementation detailsys_email_templatereplaces, not a behaviour. All ten arerepointed at the
templateDatakey carrying the same value, and nothing else undertest/changed:
test/flow-scheduled.test.tscase_number·contract_numberx2 ·subject·nametest/flow-record-change.test.tsname·first_name·last_name·subjecttest/flow-sla-ownerless-assignment.test.tscase_number, in thealertFor()correspondence filterThat is the count of failing TEST CASES that PR #1893 observed; counted as ASSERTION SITES the
split is 5 / 4 / 1 —
flow-record-changehas twoexpectlines in one test (AdaandLovelace), and the SLA suite has one filter used by two tests. The total is 10 either way,and the set of lines is the same set.
The audience, severity and "a template dot-walked a lookup" guards standing beside them are
untouched and still pass — deliveries were never broken by this conversion.
Reverse verification — the repointed assertions can still fail
A
toContainreading a key that no longer exists would pass vacuously, so this was proved, notargued. From the committed implementation, one render input in
case-sla-monitor.flow.tswasmutated (
case_numberfed from the case SUBJECT instead of the case number), proved to havereached disk (anchor text 1 to 0, injected text 0 to 1, blob hash moved off the
HEADblob),and the two suites re-run:
Restore is proved by bytes, not by an exit code: the restored blob hash equals the
HEADblobhash (
3c1537e4...) andgit diff HEADon that path is empty. The mutation is not in this PR.Verification
pnpm verify— exit 0, captured into a file before any pipe read it:Chain:
validate·typecheck·lint·lint:i18n-gate·hygiene·hygiene:tokens·build·test. Verdict lines from the run:✓ i18n lint gate: 0 i18n/missing-* issues·✓ source hygiene clean·✓ source token ratchet clean·✓ Build complete·Test Files 172 passed (172)·Tests 3685 passed | 1 skipped (3686). The 13 author-timewarnings the build prints are the pre-existing advisory set, unchanged by this PR.
Acceptance audit over the BUILT artifact (
dist/objectstack.json), both directions onevery claim:
The last row is worth its own line: it catches a translation that quietly dropped a
{{hole}},which no reference-closure check would see.
Changeset —
minor.changeset/notify-nodes-speak-the-recipients-language.md. Minor, not patch: this isuser-visible new behaviour, not a fix. Notifications that were English for everyone now arrive
in each recipient's own language, and the copy becomes administrator-editable in Studio as
sys_email_templaterows instead of needing a code change. Notskip-changeset: thecompiled artifact gains 76 rows and 19 flows change shape, so this plainly ships something.
Not
major: nothing an existing consumer depends on is removed or renamed.What was measured rather than assumed
on the pre-migration tree, and Track A has landed since. The four extra nodes are
account-approval.flow.ts(2) andlead-conversion-approval.flow.ts(2).9531cde:src/sales/flows/holds 14*.flow.tsof which 10 carry a notify node (the card's own comment already corrected 8 to 10);
src/revenue/flows/5 of which 3;src/service/flows/4 of which 2. 15 filestouched, 19 nodes.
opportunity-approval,lead-conversion-approval'ssiblings and
case-escalationeach register two flows from one file. The artifact auditcounts 22 and all 22 carry a template.
byExportName(), not an ordered array — checked againsttest/helpers/registration-lists.tsrather than assumed: the five hand-kept ordered lists areallHooks/allFlows/allSkills/CrmSharingRules/CrmSeedData, and this collectionis not one of them.
src/marketing/is untouched (it authors no notify node) and so iscontent/docs/releases/.Generated by Claude Code