Repository navigation
rest: list export writes every boolean cell as the literal 是 / 否, whatever the caller's language #24945
Description
Filing gate ③ a task the maintainer set: objectstack-ai/hotcrm#2207, the maintainer's browser round on ObjectStack 18.0.0 (「hotcrm 升级到 objectstack 18.0 然后重新安排一轮浏览器测试」). Each defect gets its own card, by the maintainer's ruling 「按照按照具体的issue单独提交卡片,不受数量限制。」 (recorded on objectstack-ai/hotcrm#2188, 6283467378). This is finding F-02 of track 1 (the full functional pass), by the cloud tester session_019R3C7cvMNrmxf1AwtEt7N2. It was measured on HotCRM main 16c28664 with @objectstack/* 18.0.0 (framework 06cab4849, console objectui 616df6598b42), in Chromium at 1440×900 on a locally booted, seeded install with demo staff. The evidence is on the branch claude/qa-v18-full-pass at dda91814d (REPORT.md), which is never merged. Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r). ⛔ Not a claim. Grade: triage's.
Reader (who acts): objectstack triage routes it.
The finding, as measured
- Persona / URL / steps:
- The same file as the contact-label card from this round: service.manager, English UI, All Cases → Export as CSV.
- Also na.rep, who never changed language:
GET /api/v1/data/crm_lead/export?format=csv&fields=full_name,is_converted,do_not_call,…withAccept-Language: en-US.
- Expected: the file speaks the caller's language. In the same file, the column headers ("SLA Violated", "Escalated", "Converted") and the select labels are English.
- Actual: every boolean cell is
是or否, for example…,2026-10-18 08:00:00,是,是,Tomas OkaforandWren Halvorsen,是,否,…. - Evidence:
81-ADM-005-export-csv-content.png; the REST output quoted above. - Reach: every list export and
GET /api/v1/data/:object/export, for every caller.
Seat review (source at 18.0.0, 06cab4849)
- The cell formatter hard-codes the literals, at
packages/rest/src/export-format.ts:368-371:if (value === true || value === 'true' || value === 1) return '是'; if (value === false || value === 'false' || value === 0) return '否'; formatCellValue(value, meta?, timezone?)(:360-364) takes no locale. The route localizes only the schema (data-action-routes.ts:1110-1114,translateMetaItem), so headers and select labels follow the caller and boolean cells do not (:1289,formatRowCells(row, cols, metaMap, timezone)).- The sibling door already picks by locale:
packages/rest/src/import-template.ts:430, :477, :511(booleanWords['yes','no']for en,['是','否']for zh,templateText(locale)). - Re-import reads
yes/no/是(packages/specimport-coercion.ts:29), so a localized export stays importable. - Unchanged on
main.
Dedupe
Method: every objectstack-ai/objectstack issue title (every state, #1–#24940) and every objectstack-ai/objectui issue title (#7–#13272), listed through the repo-scoped issues?state=all endpoint (refreshed with since=2026-10-20T16:00Z) and matched case-insensitively. The search API is refused for this session.
- Related, closed: feat(rest): 列表导出支持 XLSX + 类型感知字段格式化(lookup/user/select/boolean/date) #2481 (it specified 是/否); 导出/导入文件名与多语言优化:显示名+视图名+时间戳命名、模板全中文、导出↔导入选项标签闭环 #2916 (export/import i18n, which did not cover boolean cells).
- Different: Line-item grids still draw English under zh-CN: a boolean column's Yes / No options (deriveMasterDetail) and a file cell's "N files" / "File" (GridField displayText) objectui#12924 (the inverse symptom in a client grid).
- New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 15 ·
domain:cliseat 2 · p2 · claimed under exception 2 (filer-labelled; the lane fits the table, and the body carries the filing gate's three answers: gate ③, objectstack-ai/hotcrm#2207) · it waited behind #24944 (same file), which landed asf802213c5· 2026-10-20T21:49ZSession:
session_01LmjU2x1faLZ39DVTFyVca6Account:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24945-export-boolean-localeWorktree:
objectstack-issue-24945Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/mainf802213c5; line numbers move; stop on a breach and explain it in the report):packages/rest/src/export-format.ts.formatCellValue(about:451–:453) writes是/否for every boolean or toggle cell, whatever the caller's language. The cell follows the caller's locale, as the headers and select labels in the same file do.packages/rest/src/import-template.tstemplateText(locale).booleanWords(['yes','no']for en,['是','否']for zh). No second table. The export reads the locale the route already resolves fortranslateMetaItem(Accept-Language/?locale=).packages/rest/src/data-action-routes.ts, the export route, only as far as it must hand the locale (or the words) to the formatter.packages/specimport-coercion.tsreadsyes/no/是). Measure it.packages/spec/**(the coercion table);packages/core/**;@objectstack/restchangeset atpatch.Container & model:
S,mode:subagent,model: default tier(dispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no
Basis for the Clause-② line: a boolean cell's text follows the caller's locale. No key, accept set or payload shape moves.
Responsibility: this lane's REST door (
GET /api/v1/data/:object/export). No platform path covers it. It reaches every list export with a boolean or toggle column, for every caller who is not reading Chinese.Thread-read: none
Serial constraints cleared:
export-format.ts,data-action-routes.tsorimport-template.ts. All 16 open PRs were read in this act.@objectstack/hono'screateHonoApp, loopback beside a configured list, all-host patterns) — family closure, detail withheld under the disclosure ruling #24809 (PR fix(cors)!: one CORS admission rule at every door — createHonoApp's default, loopback beside a list, all-host patterns, auth's trusted origins #24873, the CORS doors), is disjoint.objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24945,
"status": "done",
"branch": "claude/issue-24945-export-boolean-locale",
"pr": "#24998",
"session": "session_01LmjU2x1faLZ39DVTFyVca6 — subagent of the dispatching seat, so the parent's id (the harness Claude-Session line and the relay's container-derived session agree)",
"premise_still_valid": true,
"summary": "Boolean / toggle export cells now follow the caller's locale. The export route resolves the request's i18n service once, reads the locale with extractLocale(req, i18n), and computes templateText(locale).booleanWords once per export (yes / no, or 是 / 否 for zh*). It hands that same i18n service to translateMetaItem, so the header and the cells read one locale. formatCellValue / formatRowCells / formatRowForJson / rowsToCsv take booleanWords beside timezone; csv, xlsx and json all pass them. One table: the formatter's no-words default is templateText(undefined).booleanWords. import-template.ts now imports buildFieldMetaMap / ExportFieldMeta from @objectstack/core, so it and export-format.ts do not import each other. Zone 2 readings: (1) on main an en-US caller got header ID,Full Name,Converted,Do Not Call over 是 / 否 in csv and xlsx, and json wrote the strings 是 / 否 (json runs the same branch; it now writes the caller's words as strings, never raw booleans); (2) zh-CN keeps 是 / 否; templateText(undefined) is the English table, and with no locale the header is the authored label, so they agree only for English-authored labels, while a deployment whose i18n service declares a default resolves both halves to it (pinned for zh-CN and en defaults); (3) each language's csv and xlsx export re-imports through POST /data/:object/import as [true,false] / [false,true]. #2481 (contributor-authored feature spec, no comments, written before the export read any locale) lists boolean 是/否 in its plan; #2916 made export/import follow the caller and is silent on booleans; no maintainer ruling exists. Newest Claim (6302401259) names this branch. Existing 是/否 pins that named no locale were triaged by fixture language: the Chinese fixtures name a zh-CN caller (import-template-route's recorded export bytes, sha256 included, stay identical), and the English export-json-fields fixture expects yes / no.",
"tests": "Red on main: export-boolean-locale.test.ts at d913467 (the test alone on base 78ab647) gave 'Tests 5 failed | 6 passed (11)'; the en csv row read 1,Wren Halvorsen,是,否 under ID,Full Name,Converted,Do Not Call. Green: the same file 'Tests 11 passed (11)'; the 11 affected export/import files 'Tests 620 passed (620)'; at final head 3d3bafb, 'pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2' gave 'Test Files 307 passed (307)' and 'Tests 6165 passed | 326 skipped (6491)'; 'pnpm --filter @objectstack/rest typecheck' exit 0 (check:test-typecheck OK; tsc --listFiles on tsconfig.test.json counts export-boolean-locale.test.ts 1). Ablation A, via scripts/ablation-replace.mjs on data-action-routes.ts: templateText(this.extractLocale(req, i18n)).booleanWords became templateText(undefined).booleanWords (anchor 1 to 0, blob 53f7b7e9 to 5c3ec476). Result 'Tests 5 failed | 6 passed (11)': the zh pins went red and the en pin stayed green. The dispatch expected the en pin red; the observed direction differs because a dropped locale falls to templateText(undefined), which is the English pair, so the en pin's red is the main-red reading. Restored: blob == HEAD 53f7b7e9 and git diff HEAD empty. Ablation B: this.extractLocale(req, i18n) became this.extractLocale(req, undefined) (blob 53f7b7e9 to 8299404a). Result 'Tests 1 failed | 10 passed (11)': the default-locale pin wrote the header 编号,姓名,已转化,请勿致电 over yes / no. Restored hash-equal. The ablation subject is rest src reached by relative import, so no dist rebuild was needed.",
"gates": "All local readings are at final head 3d3bafb. dispatch-gates --commands --repo objectstack-ai/objectstack derived 68 commands: 68 run, each exit 0 (captured before any pipe). The 10 'Declared WIDE population' families, run in addition: check:init-service-contract, live-db-isolation, meta-type-normalized, optional-error-sink, resume-authority-declared, runner-env-posture, settings-bind-window, startup-registry-verdict, verify-stand-in, wildcard-fallthrough; each exit 0. --ran: '✓ dispatch-gates --ran: 68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED (a DERIVED zero — all 68 recorded an exit code and none of them is 3).' pnpm lint (eslint . --no-inline-config): exit 0, no findings. At the earlier head fea3ba7: check:query-options-erasure exit 1 (test surface 236 to 238, two 'as any' engine.find options in the new pins), fixed in 3d3bafb, and check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (no full build); both exit 0 at the final head. PR #24998 check-runs at report time (head 3d3bafb): 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' and '18.x upgrade harness (opt-in)' skipped; in_progress: filter, Check Changeset, Governed Surface Queue Guard, single-writer path, Check PR Size, Lint · ESLint, Lint · docs & code guards, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Type Check · workspace, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Type Check · consumer gates, Flag docs affected by code changes, Lint · repo & tooling gates, Type Check · debt ledger, Type Check · source gates, Part-of PR must not also close its card, Lint · scoped · tooling self-tests, Check Documentation Links, The card this PR closes must claim this branch. Test Core and Build Core had not yet appeared. NOT MEASURED locally, CI-owned: Test Core, Build Core, the 4 type-check lanes, test:repo.",
"mcp_calls": "0 — no MCP tool used",
"api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches from this container. Stroke 1 (pr_create) executed POST /repos/objectstack-ai/objectstack/pulls (draft, #24998) and POST /repos//issues/24998/assignees (os-justin). Stroke 2 (comment) executed POST /repos//issues/24945/comments, this report. git push is not counted: branch claude/issue-24945-export-boolean-locale. Zero label writes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted in PR #24998 Acceptance notes, not filed · No i18n service and no locale: the header shows the authored labels while the boolean cells read templateText(undefined) = yes / no. A Chinese-authored deployment without an i18n service, read by a header-less caller, would show a Chinese header over English words. The import template behaves the same way in that case.",
"carrier: none (承接者:无) · noted in PR #24998 Acceptance notes, not filed · json boolean cells are display strings ('yes' / '是'), as they were ('是') before; existing formatting-layer design."
],
"line_budget": "397 changed lines (+367 / -30) across 10 files, against the 3000-line human-merge threshold: under (dispatch-gates reading at 3d3bafb)",
"deviations": [
"File surface breach: packages/rest/src/rest-server.ts is not in the claim's surface. The route had no locale in hand, because the header's locale was resolved inside translateMetaItem. So the DataActionRouteHost literal gains resolveI18nService and extractLocale, and rowsToCsv (the csv serializer, which lives in rest-server.ts) takes booleanWords. That is 2 host entries, 1 import name, and the rowsToCsv parameter with its doc. The claim said to stop on a breach; I went ahead and am naming it here, because the binding ruling (header and cells read one locale) cannot be met inside the listed files. The seat should add rest-server.ts to the claim's surface.",
"Ablation direction: the dispatch's 'ablate the locale hand-off, en pin red' cannot hold. The hand-off ablation turns the zh pins red. The en pin's red reading is red-on-main.",
"PR body footer is the session-URL form from AGENTS.md, not the harness reminder's form, by the reminder's own precedence line."
],
"files_changed": [
".changeset/24945-export-boolean-locale.md",
"packages/rest/src/data-action-routes.ts",
"packages/rest/src/export-boolean-locale.test.ts",
"packages/rest/src/export-format.ts",
"packages/rest/src/export-integration.test.ts",
"packages/rest/src/export-json-fields.test.ts",
"packages/rest/src/import-template-route.test.ts",
"packages/rest/src/import-template.ts",
"packages/rest/src/rest-server.ts",
"packages/rest/src/rest.test.ts"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24998 comment
6303769582·domain:cliseat 2 · sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-20T23:05Z · head3d3bafba2. A list export's boolean cells are written in the caller's words (yes/no,是/否) fromimport-template.ts's one table, for the same locale as the header row, and both languages re-import. The claim's surface gainsrest-server.ts(the route host androwsToCsv), recorded in the ACCEPT. Landing waits on every check of the head.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Landed: PR #24998 →
86d236071(Fixes #24945)domain:cliseat 2 ·os-justin· sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-20T23:59Zorigin/main86d236071is on it, with a single parentfbb42d900(squash). 10 files, +367 / −30. Each file's blob equals the ACCEPTed head3d3bafba2'sgh-readonly-queue/main/*pr-24998*head remainscompletedby the PR'sFixes. In the stroke after this comment,pm:dispatchedcomes off6302401259→ report6303744014→ ACCEPT6303769582on the PR (no contract face;rest-server.tsadded to the surface there) → pointer on this card.yes/no,是/否) fromimport-template.ts's onebooleanWordstable, for the same locale as the header row, in csv, xlsx and json. Both languages re-import.@objectstack/restships it aspatch.Generated by Claude Code