Skip to content

localization: a new organization gets its default currency from its creator's language or region (zh-CN → CNY), written as a visible Setup → Localization value; with nothing to derive from, it stays unset (maintainer ruling 乙, 2026-10-23) #25495

Closed

Description

@objectstack-fleet

维护者速读

  • 事由: 环境的默认货币(Setup → 本地化 → 财务 → 默认货币,localization.currency)在新环境里是空的。所以「动态」模式的货币字段,也就是默认模式,只显示数字,不带币种。
  • 裁定: 维护者 2026-10-23 在分诊会话里选了乙:新环境创建时,按创建者的语言或地区预填默认货币,并把这个值写进设置,看得见也能改。实现放在 objectstack:「currency: no exchange-rate / home-currency conversion — dynamic multi-currency amounts cannot be reconciled or aggregated across currencies #8345:环境的默认币种,应该在objectstack 仓库吧」「同意乙」。
  • 完成标准: 中文创建者新建的组织,默认货币是 CNY,金额显示带 ¥;推不出地区时保持空,和现在一样。

Filing gate ③ a task the maintainer set (the ruling above, given in the triage seat's chat). Filed and graded by the triage seat (session_01AavokzJ5DndAwitDXvKy4U). ⛔ Not a claim. Under the human-ruling floor this is a "new-tenant default", and that is why it went to the maintainer. It is ruled.

Reader (who acts): the domain:server seat, in one PR.

Today (objectstack origin/main)

  • packages/services/service-settings/src/manifests/localization.manifest.ts: the currency key has no platform default, and that is deliberate. Its comment says the old hard 'USD' default "surfaced an unwanted "$"/"US$" on every code-less amount". The key is tenant-scoped (scope: 'tenant').
  • Readers that already honour it:
    • the field currencyMode: 'dynamic' (the default) shows amounts in it (packages/spec/src/data/field.zod.ts, currencyMode);
    • objectui's CurrencyField, the grid and the components read it through useLocalization();
    • the server puts it on every ExecutionContext (packages/core/src/security/resolve-authz-context.ts).
  • The precedent to copy: packages/plugins/plugin-auth/src/auth-plugin.ts (:2560–2620) already gives the dev seed's workspace this host's time zone, and only when the stored value's source is still 'default'. onOrganizationCreated / afterCreateOrganization in packages/plugins/plugin-auth/src/auth-manager.ts (:715, ~:3612) is the creation seam.

Ask

  1. When an organization is created, if localization.currency has no stored value, write one derived from the creator's language or region as the server knows it. That is the creating user's stored locale, if any, else the creating request's Accept-Language. The dev reads which of the two the seam can see, and says so.
  2. The mapping: the region's ISO 4217 currency (CN → CNY, JP → JPY, GB → GBP, the euro area → EUR, US → USD, and so on). A bare language is read through its usual region (zh → CNY, ja → JPY). Use a standard table, not a hand list; the dev picks the source and names it.
  3. Nothing to derive from ⇒ write nothing. Never fall back to the manifest's default_country: 'US' or locale: 'en-US' defaults. Those are platform defaults, not the creator's choice, and using them would bring back the "$" on every amount.
  4. Never overwrite a value an administrator has set. The write is a normal settings write, so Setup → Localization shows it and can change it.
  5. The dev seed follows the same rule from the host's locale, beside the existing time-zone seeding.

Not in scope

Done when

  • A test: an organization created by a zh-CN creator stores localization.currency = 'CNY'. One created with no derivable region stores nothing. An administrator-set value is not touched.
  • A console check on a fresh local stack: a dynamic-mode currency amount shows ¥, and the value appears in Setup → Localization.
  • The currency key's manifest comment says where a new organization's value comes from.

Size: S.

Dedupe

The open objectstack-ai/objectstack issues matched on default currency, localization.currency and tenant currency: only #8345 (multi-currency, on hold, a different ask). New.


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    area:i18nThe customer's own language, across UI, metadata and notifications
    domain:serverDomain lane — seat card indexed by label:pm:seat
    and removed on Oct 23, 2026
  2. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01JVTd3PprStGa26aez5omLB
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25495-org-default-currency
    Worktree: objectstack-issue-25495
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/plugins/plugin-auth/src/** (the organization-created seam and the dev seed beside its time-zone seeding; ⛔ not the organization plugin factory inside buildPluginList in auth-manager.ts, which PR #25594 moves), packages/services/service-settings/src/manifests/localization.manifest.ts (the currency comment), new tests, one changeset (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier at 8801f2bb3: "no path-derived mandate … the tier stays the PM's per-card judgment call"), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: none
    Serial constraints cleared: PR #25594 (seat 1, #25224 cut 15) moves the organization plugin factory, afterCreateOrganization included, out of auth-manager.ts; this card stays out of that region until PR #25594 lands. No other open PR touches plugin-auth/src/auth-plugin.ts or service-settings (13 open PRs read this fire).
    Premise: re-read on origin/main 8801f2bb3: localization.manifest.ts's currency key still has no platform default, and nothing in plugin-auth/src derives a currency (git grep -i currency: no seeding hit).


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25495,
      "status": "done",
      "branch": "claude/issue-25495-org-default-currency",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25621",
      "session": "session_01JVTd3PprStGa26aez5omLB",
      "premise_still_valid": true,
      "summary": "When an organization is created through POST /organization/create, AuthPlugin now wraps AuthManagerOptions.onOrganizationCreated, after auth:configure. The host's own callback still runs, and runs first. The new organization's localization.currency is written as an ordinary tenant settings row. The language is the creator's stored sys_user.locale, else the creating request's Accept-Language (read off better-auth's endpoint-context AsyncLocalStorage, parsed by preferredLocaleFromHeader). It maps through Intl.Locale#maximize and a checked-in CLDR 48 currencyData region table (cldr-core@48.0.0; no dependency added). Nothing derivable writes nothing (no default_country/locale fallback), and a value on any rung is never overwritten. The dev seed does the same from the host's POSIX locale (LC_ALL, then LC_MONETARY, then LANG), beside the time-zone seeding. The currency manifest comment says where the value comes from. A1 holds and the seam sees BOTH sources. A2 holds. A3 NOT MEASURED: objectstack-ai/cloud is not reachable from this session (add_repo: no access); the onOrganizationCreated docblock says cloud supplies this callback, and the wrapping preserves it. The organization plugin factory was not edited. PR #25594 landed mid-run and was merged in; this diff does not touch auth-manager.ts or auth-manager-organization-plugin.ts. Console check on a live stack NOT MEASURED: the full showcase and console build was not attempted on a shared 4-core box; the PM routes it.",
      "tests": "All at head 8bce0422cd, after building the closure (pnpm turbo run build --filter='@objectstack/plugin-auth...': 29/29 tasks, VERDICT command-exit 0). pnpm --filter @objectstack/plugin-auth test: Test Files 193 passed (193), Tests 3667 passed | 10 skipped (3677). pnpm --filter @objectstack/plugin-auth typecheck: exit 0 (check:test-typecheck OK). New tests: organization-created-default-currency.test.ts, 9/9 (a real kernel, real POST /organization/create under group posture with an org-scoping fixture). zh-CN gives CNY source tenant; no derivation stores nothing; es-419 stores nothing; stored ja-JP beats a zh-CN header and gives JPY; OS_LOCALIZATION_CURRENCY=EUR is kept; a host callback's GBP is kept; dev seed LANG=zh_CN.UTF-8 gives CNY; LANG=C.UTF-8 stores nothing; an existing tenant row is kept. organization-default-currency.test.ts, 32/32. dev-admin-seed-workspace-timezone.test.ts now clears LC_ALL/LC_MONETARY/LANG. Ablation via scripts/ablation-replace.mjs with a trap restore (tests import AuthPlugin from src, so no dist leg). Leg A, Accept-Language never read (anchor x1 to x0, blob 2033a70a to 56a7862a): case 1 red, 1 failed | 8 passed. Leg B, the source!=='default' guard removed (blob to 6fd0c02e): cases 5, 6, 9 red, 3 failed | 6 passed. Restore: blob == HEAD 2033a70a and git diff HEAD empty after each leg.",
      "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 8bce0422cd derived 74 commands. All 74 ran, every one exit 0, each exit captured before any pipe. --ran (records as 'cmd :: exit N'): 'Run reconciliation — 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN'. (At the pre-merge head 907b7ef76, check:dual-build-cjs-loads exited 3 PREREQUISITE NOT MET; at 8bce0422cd it measured: 112 require entry points across 69 packages load.) eslint --no-inline-config on the 6 touched files: exit 0. check-platform-checklist not owed (no export renamed or removed). CI on PR #25621 at 8bce0422cd when the report was written: Check PR Size success, Dogfood Regression Gate success, both claim/same-issue/single-writer guards success, Check Documentation Links success. In progress: Lint · ESLint, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · docs & code guards, Lint · PM dispatch-gates, Type Check · workspace/source gates/consumer gates/debt ledger, Build Core, Test Core 1-6/6, Check Changeset, Governed Surface Queue Guard.",
      "dogfood_battery": "n/a — no `!` in the PR title and Clause-② is `no` with no (narrowing) arm",
      "mcp_calls": "1 — mcp__claude-code-remote__add_repo (objectstack-ai/cloud, access read; refused). No GitHub MCP tool was called.",
      "api_writes": "2 relay strokes plus git pushes. (1) pr_create via fleet-write/dispatch.mjs, request fw-20261024T102258Z-fe0098: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos/objectstack-ai/objectstack/issues/25621/assignees [sunhaolin]. (2) This os-dev-report comment via post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/25495/comments. git push is not REST (5 pushes to claude/issue-25495-org-default-currency). Zero labels written: none named by the dispatch, and a changeset is present, so no skip-changeset.",
      "open_questions": [
        {
          "question": "Should the single posture's Default Organization take its first owner's currency when the owner is bound to it? It is created at boot by ensureDefaultOrganizationExists with no creator, and never passes through organization/create. So on the most common self-host path (os serve, first sign-up) it still gets no currency. Only the os dev seed fills it.",
          "options": [
            "A: No change. Under the ruling, an organization with no creator has nothing to derive from. Cost: zero.",
            "B: When the Default Organization's owner is first bound (the default-org bootstrap's owner bind), apply the same rule from that owner's stored locale or sign-up request. Cost: S, in plugin-auth beside ensure-default-organization.ts. It needs the request context at the bind, which this card did not measure."
          ],
          "recommendation": "B, as a separate card if the maintainer wants the zh-CN self-host first run to show ¥. Real business need: it is the path most self-hosters take. Long-term soundness: the same single rule and the same write door. AI-safety: neutral, since no metadata surface is involved. Startup focus: one call site, no new surface. Raised as a question rather than taken, because 乙 was ruled for a new organization's creator."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed (in PR Acceptance notes) — the Accept-Language reader in organization-default-currency.ts is a separate copy of auth-manager.ts's private acceptLanguageHeader, kept apart to stay out of auth-manager.ts while PR #25594 moved it; exporting the existing reader would leave one copy · dedupe words: acceptLanguageHeader, accept-language reader, endpoint context headers",
        "carrier: triage seat · noted, not filed — A3 cloud path NOT MEASURED (objectstack-ai/cloud not reachable from this session); whether a cloud environment runtime's first organization is created through onOrganizationCreated is for triage to measure · dedupe words: cloud first organization, onOrganizationCreated cloud, environment default currency"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25495,
      "status": "done",
      "branch": "claude/issue-25495-org-default-currency",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25621",
      "session": "session_01JVTd3PprStGa26aez5omLB",
      "premise_still_valid": true,
      "summary": "ADDENDUM (PM follow-up). (1) Console check DONE, no defect found. I built the full workspace (pnpm build, 76/76) and the console at the pinned objectui SHA 2958f17e9c6a (pnpm objectui:build), and booted a fresh showcase stack: OS_TENANCY_POSTURE=group OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai OS_AUTH_MEMBERSHIP_POLICY=invite-only pnpm dev -- --fresh -p 38425 (host LANG unset). The first boot without the membership policy was refused by the walled-posture membership-policy gate, which is that gate working as designed. In Playwright Chromium with locale zh-CN, signed in as the dev admin, POST /api/v1/auth/organization/create with Accept-Language zh-CN,zh;q=0.9 created 上海分公司. GET /api/settings/localization then answered currency {value: CNY, source: tenant}. The Default Organization (no creator) answered {value: null, source: default}. The console, in 上海分公司, at /_console/apps/com.example.showcase/showcase_product: the new product's unit_price (dynamic mode, no code) renders ¥1,234.50, and every seeded price renders with ¥ (¥29.99, ¥99.00 …). Same list in the Default Organization: plain 29.99, 99.00 …. Setup → Localization (/_console/apps/setup/system/settings/localization) shows 默认货币 = CNY with the 租户 (tenant) source badge. The server, screenshots and both worktrees were removed afterwards. (2) One reader: auth-manager.ts's acceptLanguageHeader is now exported. organization-default-currency.ts calls acceptLanguageHeader(tryGetCurrentAuthEndpointContext()) and its copy is deleted (commit 40b9af0e49, +7/-16). Public .d.ts unchanged: index.ts re-exports by name, and the package exports only '.', './rate-limit-storage' and './client-address'. The organization-plugin region is untouched. PR body: NOT patched. os-dev.md says a dev writes the PR body once, at creation, and the seat writes later edits; it outranks the dispatch's 'update the PR body'. Conflict flagged. Text for the seat to write: replace the '## Console check' section with the console reading above. Delete the Acceptance-notes bullet about the second Accept-Language reader. In '## Gates', change the head to 40b9af0e49.",
      "tests": "At head 40b9af0e49: pnpm --filter @objectstack/plugin-auth test, Test Files 193 passed (193), Tests 3667 passed | 10 skipped (3677). typecheck exit 0. build exit 0 (VERDICT command-exit 0). Targeted after the refactor: organization-default-currency (32), organization-created-default-currency (9), dev-admin-seed-workspace-timezone (5), auth-email-locale (the reader's other caller, 69). Test Files 4 passed (4), Tests 115 passed (115). Earlier ablation readings (legs A and B, restored by hash) stand. No tested behaviour changed in this round, so there is no new ablation.",
      "gates": "Local at 40b9af0e49: dispatch-gates --commands derived 74. All 74 ran, every one exit 0, each exit captured before any pipe. --ran: 'Run reconciliation — 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN'. eslint --no-inline-config on auth-manager.ts and organization-default-currency.ts: exit 0. CI on PR #25621 at 40b9af0e49: all 34 non-skipped check-runs success. That includes the required five (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard), plus Check Changeset and Test Core 1-6/6. PR still draft, auto_merge null.",
      "dogfood_battery": "n/a — no `!` in the PR title and Clause-② is `no` with no (narrowing) arm",
      "mcp_calls": "1 — mcp__claude-code-remote__add_repo (first round; refused). None this round. No GitHub MCP tool was called.",
      "api_writes": "This round: 1, this addendum comment via post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/25495/comments). git push of 40b9af0e49 is not REST. Cumulative: 3 relay strokes (pr_create plus assignee, report comment, this addendum). The PR body was not PATCHed (see summary).",
      "open_questions": [
        {
          "question": "Should the single posture's Default Organization take its first owner's currency when the owner is bound to it? It is created at boot by ensureDefaultOrganizationExists with no creator, and never passes through organization/create. So on the most common self-host path (os serve, first sign-up) it still gets no currency. Only the os dev seed fills it.",
          "options": [
            "A: No change. Under the ruling, an organization with no creator has nothing to derive from. Cost: zero.",
            "B: When the Default Organization's owner is first bound (the default-org bootstrap's owner bind), apply the same rule from that owner's stored locale or sign-up request. Cost: S, in plugin-auth beside ensure-default-organization.ts. It needs the request context at the bind, which this card did not measure."
          ],
          "recommendation": "B, as a separate card if the maintainer wants the zh-CN self-host first run to show ¥. Real business need: it is the path most self-hosters take. Long-term soundness: the same single rule and the same write door. AI-safety: neutral, since no metadata surface is involved. Startup focus: one call site, no new surface. Raised as a question rather than taken, because 乙 was ruled for a new organization's creator."
        }
      ],
      "out_of_scope_findings": [
        "carrier: triage seat · noted, not filed — A3 cloud path NOT MEASURED (objectstack-ai/cloud not reachable from this session); whether a cloud environment runtime's first organization is created through onOrganizationCreated is for triage to measure · dedupe words: cloud first organization, onOrganizationCreated cloud, environment default currency"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25621 comment 6383510997 (head 40b9af0e49; console check measured; lands once CI is green on that head) · domain:server seat 2 · session_01JVTd3PprStGa26aez5omLB


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #25621 → e8de8a7ec · domain:server seat 2 · sunhaolin · session_01JVTd3PprStGa26aez5omLB · 2026-10-24T11:43Z

    reading result
    PR #25621 object merged: true, merge commit e8de8a7ece08da5875cb50186eed3b6509a367c8, merged 2026-10-24T11:42:27Z through the merge queue
    origin/main e8de8a7ec is an ancestor of origin/main (e8de8a7ece), single parent (squash)
    queue branch no gh-readonly-queue/main/* head for PR 25621 remains
    • State: closed completed by Fixes #25495 on merge. pm:dispatched stripped in this act. domain:server, area:i18n and the type label stay.
    • Delivered: a new organization's localization.currency comes from its creator's stored language, else the creating request's Accept-Language, through CLDR 48's region table. Nothing derivable means nothing is written; an existing value is never overwritten. The os dev seed follows the host locale. The console check is recorded in PR feat(plugin-auth): a new organization takes its creator's default currency (#25495) #25621's body.
    • Left open, with the maintainer: whether the creator-less Default Organization (single posture, created at boot) takes its first owner's currency when that owner is bound. Ruling 乙 names a new organization's creator; a separate card if the maintainer says yes.
    • For triage: whether a cloud environment's first organization passes through onOrganizationCreated (A3) is not measured.

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Follow-up filed · domain:server seat 2 · sunhaolin · session_01JVTd3PprStGa26aez5omLB · 2026-10-24T13:05Z: the maintainer ruled B on the open question in this card's landing comment (verbatim, in the seat's session: 「25619 C 25495 B」). The single posture's creator-less Default Organization takes its first owner's currency when that owner is bound: #25651.


    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

area:i18nThe customer's own language, across UI, metadata and notificationsdomain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions