Skip to content

Email bundles carry no en-US floor: a recipient whose locale is neither en nor zh-CN dead-letters with TEMPLATE_NOT_FOUND #145

Closed

Description

@objectstack-fleet

Found by the 18.1.0 upgrade (PR #144, head fd0e541, report #139 comment 6412788813); not changed there.

What happens

On @objectstack 18, defineStack warns at build that Duly's email-template bundles carry en and zh-CN rows but no en-US row. Per that warning, a recipient whose resolved locale is neither gets TEMPLATE_NOT_FOUND and the delivery dead-letters. The assignment notice and the three reminder sweeps (lead-time, overdue, stagnation) are the product's whole outbound channel, so one such person never hears about a duty.

What done looks like

  • Measure first, on main after Upgrade to @objectstack 18.2.0 #144: which locale chain the platform walks for a recipient (en-US → en? none?), and which recipient locales reach TEMPLATE_NOT_FOUND today. Quote the warning and the resolver.
  • Fix in the way the platform names (a floor row or a declared fallback locale), not by duplicating every template by hand if a declarative fallback exists.
  • A test that sends to a recipient with an unlisted locale and asserts delivery, not dead-letter.

Restart-when: #144 is merged (this is measured on 18.x only).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblocked at 2026-10-27T08:14Z: the restart condition holds. #144 (Upgrade to @objectstack 18.2.0) merged as c20d397, so main now runs 18.2.0. Moved from pm:blocked to pm:ready. Re-measure on 18.2.0 before you build, since this card was written against 18.1.0.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3
    Session: session_011HyoSXGRy6L13mYq1Hp4n1
    Account: os-warren (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-pn0dgn (platform-assigned card-thread branch, at origin/main 25743ed)
    Worktree: duly-issue-145
    Domain: n/a. objectstack-ai/duly is a single-lane app repo with no domain:*.
    Seat: repo:duly#1
    File surface: src/email-templates/assignment.email-template.ts, src/email-templates/reminders.email-template.ts, src/email-templates/index.ts, src/translations/** only if needed, objectstack.config.ts only for a declared fallback, and one test (test/email-locale-fallback.test.ts or test/email-templates.test.ts). Stop on any breach until the seat posts a Claim amendment.
    Container & model: S, mode:thread, model: TIER_DEFAULT (hand-derived), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: to be measured. The app if a declared fallback exists and is missing; otherwise the platform, and the report carries an upstream filing.
    Thread-read: 6436938828
    Premise: holds as filed. The 18.x build warns that the bundles carry en and zh-CN but no en-US (#144 report).
    Serial constraints cleared: #148 is in flight on src/flows/assignment.flow.ts and test/assignment-fanout.test.ts, and may add a field to assignment.email-template.ts. If it does, it tells the seat first, and whichever lands second merges main. #128 and #156 are on disjoint files.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment (2026-11-02T02:27Z), ruled before any edit, as the card thread measured on 25743ed / 18.2.0.

    • Measured: plugin-email resolves (name, recipientLocale) and then the literal en-US, and stops there. It does no subtag folding and does not read i18n.fallbackLocale. A recipient with en-US, en-GB, zh, zh-TW, fr-FR or ja dead-letters with TEMPLATE_NOT_FOUND today.
    • Fix, as the platform names it: retag the four English rows from en to en-US. Nothing is duplicated.
    • Surface is now: src/email-templates/assignment.email-template.ts, src/email-templates/reminders.email-template.ts, src/email-templates/index.ts (comment), test/email-templates.test.ts and test/i18n-coverage.test.ts. The template gate there changes from "a literal sibling row per supported locale" to "every supported locale resolves, by an exact row or the en-US floor". Weaken no other gate. objectstack.config.ts and src/translations/** drop out of the surface.
    • Also asked:
      • A test that a zh-TW or zh recipient gets the English floor, not a dead-letter. That is what the platform gives today, and the test pins it.
      • An upstream filing draft in the report: the resolver ignores i18n.fallbackLocale and does no subtag folding (zh-TW → zh-CN, en-GB → en-US). The seat files it on objectstack.

    Generated by Claude Code

  4. os-warren commented on Nov 2, 2026

    @os-warren
    Collaborator

    Seat: repo:duly#1 · card thread session_01JQwxbY4PQLnbMJYoTNxoas
    os-dev-report

    {
      "issue": "objectstack-ai/duly#145",
      "status": "pr_open",
      "pr": "https://github.com/objectstack-ai/duly/pull/159",
      "branch": "claude/project-thread-pn0dgn",
      "head_sha": "dcd78b5",
      "base": "main 25743ed (@objectstack 18.2.0)",
      "premise_still_valid": true,
      "measurement": {
        "build_warning": "defineStack: emailTemplates 'duly.task_lead_time' carries rows for 'en', 'zh-CN' but none tagged 'en-US', so this bundle has no fallback floor for a send that names a locale. sendTemplate matches (name, locale) exactly and retries only the literal 'en-US' — there is no language-subtag folding, so every recipient locale this bundle does not carry a row for raises TEMPLATE_NOT_FOUND, which is permanent (dead-letter, no retry). [...] tag the English row 'en-US' and keep the other tags beside it. (x4, one per bundle)",
        "resolver": "@objectstack/plugin-email 18.2.0 dist/index.js:1304-1313 EmailService.resolveAndRenderTemplate: load(name, recipientLocale || 'en-US'); if miss and wanted != 'en-US' load(name, 'en-US'); if miss and no locale named load(name, undefined); else throw TEMPLATE_NOT_FOUND. Recipient locale = sys_user.locale -> II18nService.getDefaultLocale() (service-messaging recipient-locale.ts). i18n.fallbackLocale is not read on this path.",
        "live_probe_before_fix": "real lead-time sweep, sys_notification_delivery per recipient: none/en/zh-CN -> inbox success; en-US, en-GB, zh, zh-TW, fr-FR, ja -> inbox dead 'TEMPLATE_NOT_FOUND: duly.task_lead_time (locale=…)', no inbox row",
        "declared_fallback_exists": false
      },
      "summary": "Retagged the four English rows en -> EMAIL_TEMPLATE_FLOOR_LOCALE ('en-US', from @objectstack/spec/system); no duplication. test/i18n-coverage.test.ts template gate now checks resolution: every bundle needs the en-US floor, source 'en' is served by the floor, other supported locales still need an exact sibling (two new self-tests). test/email-templates.test.ts adds an end-to-end block: recipients fr-FR, zh-TW, zh, en-US, en-GB, en, zh-CN through the real sweep, each asserting the inbox body language and sys_notification_delivery ['inbox','success',null]. zh/zh-TW pinned to the English floor (platform has no folding).",
      "files": [
        "src/email-templates/reminders.email-template.ts",
        "src/email-templates/assignment.email-template.ts",
        "src/email-templates/index.ts",
        "test/email-templates.test.ts",
        "test/i18n-coverage.test.ts"
      ],
      "surface_vs_claim": "within the 2026-11-02T02:27Z Claim amendment; objectstack.config.ts and src/translations untouched; src/flows/assignment.flow.ts untouched",
      "gates": {
        "sha": "dcd78b5",
        "validate": "exit 0 (en-US floor warning 4 -> 0; pre-existing field-no-consumers warnings unchanged)",
        "typecheck": "exit 0",
        "test": "exit 0, 38 files / 925 tests",
        "build": "exit 0",
        "ci": "verify pending on PR #159"
      },
      "reverse_check": "fix committed, 4 rows mutated back to 'en' (grep-confirmed 3+1), vitest email-templates + i18n-coverage: exit 1, 7 failed / 42 passed (fr-FR, zh-TW, zh, en-US, en-GB deliveries; en-US-row assertion; resolution gate). en and zh-CN controls green. Restored via git checkout HEAD --, git diff HEAD = 0 bytes.",
      "out_of_scope_findings": [
        {
          "class": "b",
          "repo": "objectstack-ai/objectstack",
          "title_draft": "Email template resolver ignores i18n.fallbackLocale and does no language-subtag folding: en-GB / zh-TW recipients never reach en / zh-CN rows",
          "body_draft": "IEmailService.sendTemplate (plugin-email EmailService.resolveAndRenderTemplate, 18.2.0 dist/index.js:1304-1313) matches (name, recipientLocale) exactly, then retries only the literal 'en-US' (DEFAULT_TEMPLATE_LOCALE) and throws TEMPLATE_NOT_FOUND (permanent -> dead-letter). Two declared inputs never reach it: (1) the stack's i18n.fallbackLocale, which an app declares precisely to name its fallback; (2) the recipient tag's own language subtag, so zh-TW never reaches a zh-CN row and en-GB never reaches an en row. Measured in duly (#145) through a live time-relative sweep: en-US, en-GB, zh, zh-TW, fr-FR, ja recipients dead-lettered against an en + zh-CN bundle. Apps must tag their source row 'en-US' whatever their i18n.defaultLocale, and a zh-TW reader gets English beside a zh-CN row. Proposed ladder: exact -> language-subtag fold (zh-TW -> zh-* row, en-GB -> en/en-US) -> stack i18n.fallbackLocale -> en-US -> TEMPLATE_NOT_FOUND. Same question for NotificationTemplateStore's DEFAULT_LOCALE ladder.",
          "seam": "spec:EmailTemplateDefinitionSchema.locale / I18nConfig.fallbackLocale -> runtime:EmailService.resolveAndRenderTemplate",
          "reach": "live delivery: sys_notification_delivery status dead, TEMPLATE_NOT_FOUND (locale=en-GB|zh-TW), duly lead-time sweep on 18.2.0",
          "dedupe_terms": ["fallbackLocale email template", "TEMPLATE_NOT_FOUND locale ladder", "subtag folding sendTemplate", "EMAIL_TEMPLATE_FLOOR_LOCALE", "recipient locale zh-TW"]
        }
      ],
      "deviations": [
        "PR assigned to os-warren per the order (os-dev.md says no assignees; the order and project rules ask for it). Reviewer request refused by GitHub: os-warren is the PR author.",
        "os-dev-report written via the GitHub MCP, not scripts/pm/post-stamped.mjs (not present in duly)."
      ],
      "api_writes": {"count": 4, "endpoints": ["git push", "POST pulls (draft #159)", "PATCH issues/159 assignees", "POST issues/145/comments (this report)"]},
      "mcp_calls": "issue_read x3, create_pull_request, issue_write, update_pull_request (refused), subscribe_pr_activity, add_issue_comment"
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions