diff --git a/README.md b/README.md
index d7ad999..edd7e0f 100644
--- a/README.md
+++ b/README.md
@@ -14,16 +14,21 @@ Built on the [ObjectStack](https://github.com/objectstack-ai/objectstack) 17.x m
Organization, accounts, permissions, audit logging, import/export, list and form UI come from the
platform; six server-side code points carry what is specific to assessment.
-> **Language note.** The application UI and the delivery documents under `docs/` are in Simplified
-> Chinese — this system was built for a Chinese enterprise, and the copy is part of the domain
-> modelling rather than an afterthought. The platform is configured for `zh-CN` and `en`
-> (`objectstack.config.ts`), but the English bundle is currently a **stub fallback**: 32 labels
-> across 7 objects, against 320 labels across 21 objects for `zh-CN`. Switching to English today
-> gives a mixed-language UI, and view, action, page and error copy have no English at all. A full
-> English UI is not yet in scope — see [Internationalization](#internationalization).
-> This README is the English entry point.
+> **Language note.** This system was built for a Chinese enterprise, so the domain copy is authored
+> in Simplified Chinese and the delivery documents under `docs/` are Chinese throughout. The UI
+> itself runs in English: every object, field, picklist value, list view, action and navigation
+> label is translated (530 keys). A few surfaces are not yet covered — see
+> [Internationalization](#internationalization) for exactly which.
-
+
+
+*One assessment cycle. Solid arrows are the sheet moving between people; dashed arrows are the five
+points where the system acts on its own.*
+
+
+
+*Entry sheets queued by workflow state. The record names are Chinese because the bundled demo
+dataset is — the interface is not.*
## Why this exists
@@ -145,14 +150,11 @@ grants from historical plans stay untouched. Once a plan is **closed or archived
## Business process
-Four workflow steps by default; the steps are configurable per plan.
+Four workflow steps by default; the steps are configurable per plan. Every transition is decided by
+a server-side hook — buttons only write a pending action, so writing the status field directly is
+refused the same way.
-```
-Plan draft ─publish (integrity check)─▶ entry sheets + lines (frozen copy of target, weight, scoring rule)
-Entering ─submit─▶ Branch verification (parallel check tasks, advances when all confirm)
- ─▶ HR review ─▶ Executive approval ─▶ Passed ─archive─▶ Archived (immutable snapshot)
-Any review step ─reject (reason required)─▶ previous step
-```
+
- **Scoring engine** — `src/lib/scoring.ts` (linear / step / range interpolation / CEL formula).
Saving an actual value scores it immediately and persists a human-readable calculation trace.
@@ -201,28 +203,41 @@ Full detail in [CLAUDE.md](CLAUDE.md). The load-bearing ones:
as issues on objectstack-ai/objectstack (symptom, minimal repro, expected capability, version) —
no fix PRs there. Application-side workarounds must be environment-gated and annotated with the
platform issue they track, and deleted once it is fixed.
-- User-facing copy is Simplified Chinese. The `zh-CN` bundle is generated from metadata labels by
- `pnpm i18n:extract`; never hand-edit its entries. The `en` bundle is hand-maintained.
+- Metadata labels are authored in Simplified Chinese (`Field.number({ label: '实际值', … })`), which
+ fills the `zh-CN` bundle verbatim — never hand-edit its entries. The `en` leaves are translated by
+ hand; change a Chinese label and you must re-translate its English counterpart yourself.
## Internationalization
`objectstack.config.ts` declares `defaultLocale: 'zh-CN'`, `supportedLocales: ['zh-CN', 'en']` and
-`fallbackLocale: 'zh-CN'`, so the English path works — but it is far from complete:
+`fallbackLocale: 'zh-CN'`. Both bundles carry **530 keys** and are generated by `pnpm i18n:extract`
+(`os i18n extract --locales=zh-CN,en --fill=default`), with `pnpm verify` failing on a stale bundle.
-| Bundle | Objects | Labels | Maintenance |
-|---|---|---|---|
-| `zh-CN` | 21 | 320 | Generated by `pnpm i18n:extract` from metadata labels; entries must not be hand-edited |
-| `en` | 7 | 32 | Hand-maintained |
+| Bundle | Keys | How the leaves are filled |
+|---|---|---|
+| `zh-CN` | 530 | Copied verbatim from the metadata labels — **never hand-edit** |
+| `en` | 530 | Seeded from the Chinese source, then translated by hand |
+
+**Covered in English:** object, field and picklist labels; object descriptions; list views; record
+actions; app navigation; dashboard titles and widget titles; page titles.
+
+**Not covered, and why:**
+
+| Surface | Why | Where it lives |
+|---|---|---|
+| Dataset field labels (the captions under dashboard metrics) | Outside the extractor's scope | `src/datasets/index.ts` |
+| Dashboard filter labels and their options | Outside the extractor's scope | `src/dashboards/index.ts` |
+| Workbench page body copy and block headings | Page *component properties* are not extracted; only a page's label/title/subtitle are | `src/pages/index.ts` |
+| The three-part error messages raised by hooks | Plain strings in code, not metadata | `src/hooks/*.ts` |
+| The bundled demo dataset (org units, indicator names, units) | It is data, not copy | `src/data/*.ts` |
-Anything the `en` bundle misses falls back to Chinese, so an English session today is mixed-language.
-Beyond object and field labels, **view tabs, action labels, workbench page copy, dashboards, reports
-and the three-part error messages raised in hooks have no English translation** — metadata labels are
-authored in Chinese (`Field.number({ label: '实际值', … })`), and English exists only where the bundle
-covers it.
+The first three are platform extractor gaps rather than missing translations. Closing the last one
+would mean an English demo seed profile alongside the existing two. Contributions welcome — please
+raise an issue first so the scope can be agreed.
-Bringing the English UI up to parity is a self-contained piece of work — regenerate the `en` skeleton
-with `os i18n extract --locales=en`, translate the entries, then extend to views, pages and hook
-messages. Contributions welcome; please raise an issue first so the scope can be agreed.
+**Re-translating after a label change.** Merge only fills *gaps*: if you change a Chinese metadata
+label, the matching English leaf already holds a translation, so it is left alone and silently goes
+stale. Nothing warns you. Re-translate it by hand when you change the source.
## Documentation
@@ -237,6 +252,7 @@ All documents are in Simplified Chinese.
| [docs/手册/KPI考核管理系统-操作手册.md](docs/手册/KPI考核管理系统-操作手册.md) | Operations manual, one chapter per business role, with real UI screenshots. Word build via `pnpm docs:manual` |
| [docs/汇报/KPI考核管理系统-解决方案汇报.html](docs/汇报/KPI考核管理系统-解决方案汇报.html) | Solution briefing: nine diagrams covering the process, scoring, aggregation and permission model, plus 26 UI screenshots. Self-contained single file via `pnpm docs:report` |
| [scripts/build-report-deck.mjs](scripts/build-report-deck.mjs) | Generates a 19-slide briefing deck from the same material, with speaker notes. See the script header for usage and dependencies |
+| [docs/readme/diagrams.en.html](docs/readme/diagrams.en.html) | Source for the two English diagrams above. Re-export by screenshotting each `figure` with a headless browser at 2× |
| [CLAUDE.md](CLAUDE.md) | Development conventions and delivery-process checklist |
## Contributing
diff --git a/docs/readme/diagrams.en.html b/docs/readme/diagrams.en.html
new file mode 100644
index 0000000..fbd79bd
--- /dev/null
+++ b/docs/readme/diagrams.en.html
@@ -0,0 +1,228 @@
+
+
+
KPI Assessment — README diagrams (English)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ① CONFIGURE & PUBLISH
+ ② ENTER
+ ③ VERIFY & REVIEW
+ ④ ARCHIVE
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HR Review
+ Department Reporter
+ Branch Checker
+ Executive Leader
+ System · automatic
+
+
+
+ Configure Plan
+ indicators · subjects
+ weights · targets · steps
+
+
+ Publish Plan
+ config freezes here
+ changes need a new copy
+
+
+ HR Review
+ approve, or reject
+ with a written reason
+
+
+ Archive
+ the period is sealed
+ fix in the next one
+
+
+
+ Enter & Score
+ actuals row by row
+ or import from Excel
+
+
+ Submit
+ blocked while any
+ actual is still blank
+
+
+
+ Branch Verification
+ all branches in parallel
+ confirm, or dispute
+
+
+
+ Executive Approval
+ only subjects they
+ oversee · final say
+
+
+
+ Integrity check
+ generates entry sheets
+ freezes target + rule copy
+
+
+ Scores on save
+ completion · rate · score
+ plus a calculation trace
+
+
+ Creates check tasks
+ one per branch that
+ the plan involves
+
+
+ Builds results
+ four dimensions + rank
+ drillable to the line
+
+
+ Writes the snapshot
+ SHA-256 checksum
+ everything turns read-only
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Plan published
+ one sheet per subject
+
+
+
+ Entering
+ Department Reporter
+
+
+ Branch Verification
+ Branch Checker
+
+
+ Under HR Review
+ HR Reviewer
+
+
+ Executive Approval
+ Executive Leader
+
+
+ Passed
+ results generated
+
+
+ Archived
+ immutable
+
+ Submit
+ gate: every actual filled in
+ position: Department Reporter
+
+
+ All branches confirm
+ one open dispute stops it
+ skipped if no branch verifies
+
+
+ Approve
+ position: HR Reviewer
+ comment optional
+
+
+ Approve
+ only the subject's own leader
+ builds the four dimensions
+
+
+ Archive
+ position: HR Reviewer
+ snapshot + SHA-256
+
+
+
+ Reject · reason required · back one step
+
+
+ Reject → every branch verifies again
+
+
+ Actuals are frozen from submission onward — corrections go through a Data Adjustment request
+
+
diff --git a/docs/readme/flow-swimlane.png b/docs/readme/flow-swimlane.png
new file mode 100644
index 0000000..b8f94f3
Binary files /dev/null and b/docs/readme/flow-swimlane.png differ
diff --git a/docs/readme/state-machine.png b/docs/readme/state-machine.png
new file mode 100644
index 0000000..9d6f17f
Binary files /dev/null and b/docs/readme/state-machine.png differ
diff --git a/docs/readme/ui-entry-sheets.png b/docs/readme/ui-entry-sheets.png
new file mode 100644
index 0000000..5069143
Binary files /dev/null and b/docs/readme/ui-entry-sheets.png differ
diff --git a/package.json b/package.json
index 6260b99..0ff5c47 100644
--- a/package.json
+++ b/package.json
@@ -18,8 +18,8 @@
"validate": "objectstack validate",
"typecheck": "tsc --noEmit",
"test": "vitest run",
- "i18n:extract": "objectstack i18n extract --locales=zh-CN --no-metadata-forms --no-objects-only --filter=kpi_ --out=src/translations",
- "i18n:extract:check": "objectstack i18n extract --locales=zh-CN --no-metadata-forms --no-objects-only --filter=kpi_ --out=src/translations --check",
+ "i18n:extract": "objectstack i18n extract --locales=zh-CN,en --fill=default --no-metadata-forms --no-objects-only --filter=kpi_ --out=src/translations",
+ "i18n:extract:check": "objectstack i18n extract --locales=zh-CN,en --fill=default --no-metadata-forms --no-objects-only --filter=kpi_ --out=src/translations --check",
"verify": "pnpm validate && pnpm typecheck && pnpm test && pnpm i18n:extract:check",
"e2e": "node scripts/e2e-flow.mjs",
"docs:docx": "node scripts/md2docx.cjs docs/00-设计方案.md docs/交付/KPI考核管理系统-设计方案-V1.0.docx",
diff --git a/src/translations/en.objects.generated.ts b/src/translations/en.objects.generated.ts
new file mode 100644
index 0000000..2cf17de
--- /dev/null
+++ b/src/translations/en.objects.generated.ts
@@ -0,0 +1,1367 @@
+// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Auto-generated by 'os i18n extract' for locale 'en'.
+ * Edit translations in place; re-run extract (with --merge) to fill new gaps.
+ * Do not hand-edit the structure — only the leaf string values.
+ * Merge only fills gaps: correcting a source label/description does not
+ * push the correction into a leaf here that already holds a translation —
+ * a present-but-stale string is not a gap, so it is left as-is. Re-translate
+ * it by hand when its source changes; nothing here or in `os i18n check`
+ * tells a leaf a translator updated on purpose from one nobody has looked
+ * at since the source moved.
+ */
+
+import type { TranslationData } from '@objectstack/spec/system';
+
+export const enTranslations: TranslationData = {
+ objects: {
+ kpi_adjustment: {
+ label: "Data Adjustment",
+ pluralLabel: "Data Adjustment",
+ description: "A request to correct a submitted actual value (source data) or score (computed result).",
+ fields: {
+ name: {
+ label: "Adjustment No."
+ },
+ sheet: {
+ label: "Entry Sheet"
+ },
+ plan: {
+ label: "Assessment Plan"
+ },
+ subject: {
+ label: "Subject"
+ },
+ line: {
+ label: "Adjusted Entry Line"
+ },
+ adjust_type: {
+ label: "Adjustment Type",
+ options: {
+ source: "Source Data (Actual Value)",
+ result: "Computed Result (Score)"
+ }
+ },
+ old_value: {
+ label: "Value Before"
+ },
+ new_value: {
+ label: "Value After"
+ },
+ reason: {
+ label: "Adjustment Reason"
+ },
+ status: {
+ label: "Status",
+ options: {
+ draft: "Draft",
+ submitted: "Awaiting Approval",
+ approved: "Approved",
+ rejected: "Declined"
+ }
+ },
+ requested_by: {
+ label: "Requested By"
+ },
+ decided_by: {
+ label: "Approved By"
+ },
+ decided_at: {
+ label: "Approved At"
+ },
+ decision_reason: {
+ label: "Approval Comment"
+ },
+ applied_at: {
+ label: "Applied At"
+ }
+ },
+ _actions: {
+ kpi_adjustment_approve: {
+ label: "Approve & Apply",
+ description: "Approving corrects the line and rescores it immediately. Approve?",
+ successMessage: "Adjustment approved and applied; the score has been recalculated.",
+ params: {
+ decision_reason: {
+ label: "Approval Comment (optional)"
+ }
+ }
+ },
+ kpi_adjustment_reject: {
+ label: "Decline",
+ successMessage: "Adjustment request declined.",
+ params: {
+ decision_reason: {
+ label: "Decline Reason"
+ }
+ }
+ },
+ kpi_adjustment_submit: {
+ label: "Submit for Approval",
+ successMessage: "Adjustment request submitted; awaiting HR approval."
+ }
+ },
+ _views: {
+ default: {
+ label: "Data Adjustment"
+ },
+ pending: {
+ label: "Awaiting Approval"
+ }
+ },
+ _sections: {
+ request: {
+ label: "Adjustment Request"
+ }
+ }
+ },
+ kpi_bonus: {
+ label: "Bonus & Penalty",
+ pluralLabel: "Bonus & Penalty",
+ description: "Bonus or penalty points recorded outside the indicator scores.",
+ fields: {
+ sheet: {
+ label: "Entry Sheet"
+ },
+ title: {
+ label: "Item"
+ },
+ bonus_type: {
+ label: "Type",
+ options: {
+ add: "Bonus",
+ deduct: "Penalty"
+ }
+ },
+ points: {
+ label: "Points"
+ },
+ signed_points: {
+ label: "Effective Points"
+ },
+ reason: {
+ label: "Basis"
+ },
+ status: {
+ label: "Status",
+ options: {
+ draft: "Awaiting Approval",
+ approved: "Approved",
+ rejected: "Declined"
+ }
+ },
+ approved_by: {
+ label: "Approved By"
+ },
+ approved_at: {
+ label: "Approved At"
+ }
+ },
+ _actions: {
+ kpi_bonus_approve: {
+ label: "Approve",
+ successMessage: "Bonus/penalty approved and included in the final score."
+ },
+ kpi_bonus_reject: {
+ label: "Decline",
+ successMessage: "Bonus/penalty declined."
+ }
+ },
+ _views: {
+ default: {
+ label: "Bonus & Penalty"
+ },
+ pending: {
+ label: "Awaiting Approval"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Bonus & Penalty"
+ }
+ }
+ },
+ kpi_check_task: {
+ label: "Check Task",
+ pluralLabel: "Check Task",
+ description: "Parallel verification of an entry sheet by each branch: confirm, or raise a dispute.",
+ fields: {
+ name: {
+ label: "Task Name"
+ },
+ sheet: {
+ label: "Entry Sheet"
+ },
+ plan: {
+ label: "Assessment Plan"
+ },
+ branch: {
+ label: "Checking Branch"
+ },
+ status: {
+ label: "Verification Status",
+ options: {
+ pending: "Awaiting Verification",
+ confirmed: "Confirmed",
+ disputed: "Disputed"
+ }
+ },
+ comment: {
+ label: "Verification Comment"
+ },
+ decided_by: {
+ label: "Checked By"
+ },
+ decided_at: {
+ label: "Checked At"
+ }
+ },
+ _actions: {
+ kpi_check_confirm: {
+ label: "Confirm as Correct",
+ successMessage: "Verified as correct.",
+ params: {
+ comment: {
+ label: "Verification Comment (optional)"
+ }
+ }
+ },
+ kpi_check_dispute: {
+ label: "Raise Dispute",
+ successMessage: "Dispute recorded. Settle it with the reporting department, then confirm.",
+ params: {
+ comment: {
+ label: "Dispute Detail"
+ }
+ }
+ }
+ },
+ _views: {
+ default: {
+ label: "All Check Tasks"
+ },
+ pending: {
+ label: "My Verifications"
+ },
+ disputed: {
+ label: "Disputed"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Check Task"
+ }
+ }
+ },
+ kpi_dispute: {
+ label: "Indicator Dispute",
+ pluralLabel: "Indicator Dispute",
+ description: "A dispute raised by a branch against an indicator assignment, and how the business department resolved it.",
+ fields: {
+ name: {
+ label: "Dispute No."
+ },
+ plan_indicator: {
+ label: "Disputed Assignment"
+ },
+ raised_unit: {
+ label: "Raised By Unit"
+ },
+ raised_by: {
+ label: "Raised By"
+ },
+ reason: {
+ label: "Dispute Reason"
+ },
+ status: {
+ label: "Status",
+ options: {
+ open: "Open",
+ accepted: "Upheld",
+ rejected: "Rejected"
+ }
+ },
+ resolution: {
+ label: "Resolution"
+ },
+ resolved_by: {
+ label: "Resolved By"
+ },
+ resolved_at: {
+ label: "Resolved At"
+ }
+ },
+ _actions: {
+ kpi_dispute_accept: {
+ label: "Uphold",
+ successMessage: "Dispute upheld — adjust the indicator assignment accordingly.",
+ params: {
+ resolution: {
+ label: "Resolution"
+ }
+ }
+ },
+ kpi_dispute_reject: {
+ label: "Dismiss",
+ successMessage: "Dispute closed as dismissed.",
+ params: {
+ resolution: {
+ label: "Resolution"
+ }
+ }
+ }
+ },
+ _views: {
+ default: {
+ label: "All Disputes"
+ },
+ open: {
+ label: "Open"
+ }
+ },
+ _sections: {
+ raise: {
+ label: "Dispute Detail"
+ },
+ resolve: {
+ label: "Resolution"
+ }
+ }
+ },
+ kpi_entry_line: {
+ label: "Entry Line",
+ pluralLabel: "Entry Lines",
+ description: "One indicator's actual value, completion rate, score and calculation trace.",
+ fields: {
+ sheet: {
+ label: "Entry Sheet"
+ },
+ plan_indicator: {
+ label: "Source Assignment"
+ },
+ indicator: {
+ label: "Indicator"
+ },
+ indicator_name: {
+ label: "Indicator"
+ },
+ unit: {
+ label: "Unit"
+ },
+ direction: {
+ label: "Direction",
+ options: {
+ positive: "Higher is Better",
+ negative: "Lower is Better"
+ }
+ },
+ scoring_method: {
+ label: "Scoring Method",
+ options: {
+ linear: "Linear",
+ step: "Step",
+ range: "Range Interpolation",
+ formula: "Custom Formula"
+ }
+ },
+ target_value: {
+ label: "Target"
+ },
+ weight: {
+ label: "Weight (%)"
+ },
+ actual_value: {
+ label: "Actual"
+ },
+ completion_rate: {
+ label: "Completion (%)"
+ },
+ score_rate: {
+ label: "Score Rate (%)"
+ },
+ score: {
+ label: "Score"
+ },
+ adjusted_score: {
+ label: "Adjusted Score"
+ },
+ final_score: {
+ label: "Final Score"
+ },
+ is_adjusted: {
+ label: "Adjusted"
+ },
+ adjust_type_applied: {
+ label: "Adjustment Type",
+ options: {
+ source: "Source Data (Actual Value)",
+ result: "Computed Result (Score)"
+ }
+ },
+ calc_trace: {
+ label: "Calculation Trace"
+ },
+ last_adjustment: {
+ label: "Latest Adjustment"
+ },
+ remark: {
+ label: "Remark"
+ }
+ },
+ _views: {
+ default: {
+ label: "Entry Line"
+ },
+ unfilled: {
+ label: "Missing Actuals"
+ }
+ },
+ _sections: {
+ entry: {
+ label: "Entry"
+ }
+ }
+ },
+ kpi_entry_sheet: {
+ label: "Entry Sheet",
+ pluralLabel: "Entry Sheets",
+ description: "One subject's indicator entries and review history under one plan.",
+ fields: {
+ name: {
+ label: "Sheet"
+ },
+ plan: {
+ label: "Plan"
+ },
+ subject: {
+ label: "Subject"
+ },
+ subject_type: {
+ label: "Subject Type",
+ options: {
+ department: "HQ Department",
+ branch: "Branch"
+ }
+ },
+ status: {
+ label: "Status",
+ options: {
+ draft: "Entering",
+ submitted: "Submitted",
+ branch_checking: "Under Branch Verification",
+ hr_reviewing: "Under HR Review",
+ leader_approving: "Under Executive Approval",
+ approved: "Passed",
+ archived: "Archived"
+ }
+ },
+ current_step: {
+ label: "Current Step No."
+ },
+ pending_action: {
+ label: "Pending Action (written by buttons)",
+ options: {
+ submit: "Submit",
+ approve: "Approve",
+ reject: "Reject",
+ archive: "Archive"
+ }
+ },
+ action_reason: {
+ label: "Action Reason / Comment"
+ },
+ last_reject_reason: {
+ label: "Latest Rejection Reason"
+ },
+ submitted_at: {
+ label: "Submitted At"
+ },
+ submitted_by: {
+ label: "Submitted By"
+ },
+ approved_at: {
+ label: "Final Approval At"
+ },
+ approved_by: {
+ label: "Final Approver"
+ },
+ archived_at: {
+ label: "Archived At"
+ },
+ line_count: {
+ label: "Indicators"
+ },
+ weight_total: {
+ label: "Total Weight (%)"
+ },
+ indicator_score: {
+ label: "Indicator Score Total"
+ },
+ bonus_total: {
+ label: "Bonus & Penalty Total"
+ },
+ total_score: {
+ label: "Final Score"
+ },
+ remark: {
+ label: "Entry Notes"
+ }
+ },
+ _actions: {
+ kpi_sheet_approve: {
+ label: "Approve",
+ successMessage: "Approved; the sheet moves to the next step.",
+ params: {
+ reason: {
+ label: "Review Comment (optional)"
+ }
+ }
+ },
+ kpi_sheet_archive: {
+ label: "Archive",
+ confirmText: "Archiving generates an immutable snapshot and locks the data for good. Archive?",
+ successMessage: "Archived; a snapshot has been generated."
+ },
+ kpi_sheet_reject: {
+ label: "Reject",
+ successMessage: "Rejected; the entry sheet returns to the previous step.",
+ params: {
+ reason: {
+ label: "Rejection Reason"
+ }
+ }
+ },
+ kpi_sheet_submit: {
+ label: "Submit Entry",
+ confirmText: "Submitting freezes the entered data and starts verification and review. Submit?",
+ successMessage: "Entry sheet submitted; it now enters verification and review."
+ }
+ },
+ _views: {
+ default: {
+ label: "All Entry Sheets"
+ },
+ filling: {
+ label: "Entering"
+ },
+ checking: {
+ label: "Under Branch Verification"
+ },
+ hr: {
+ label: "Awaiting HR Review"
+ },
+ leader: {
+ label: "Awaiting Executive Approval"
+ },
+ approved: {
+ label: "Passed"
+ },
+ archived: {
+ label: "Archived"
+ },
+ board: {
+ label: "Workflow Board"
+ }
+ },
+ _sections: {
+ head: {
+ label: "Entry Sheet"
+ },
+ flow: {
+ label: "Workflow"
+ }
+ }
+ },
+ kpi_indicator: {
+ label: "Indicator",
+ pluralLabel: "Indicators",
+ description: "Indicator definition, data source and scoring rule, with a traceable version chain.",
+ fields: {
+ code: {
+ label: "Code"
+ },
+ name: {
+ label: "Name"
+ },
+ category: {
+ label: "Category",
+ options: {
+ business: "Business Performance",
+ management: "Management Effectiveness",
+ service: "Service Quality",
+ compliance: "Safety & Compliance",
+ other: "Other"
+ }
+ },
+ owner_unit: {
+ label: "Department Segment"
+ },
+ data_source: {
+ label: "Data Source",
+ options: {
+ manual: "Manual Entry",
+ import: "Bulk Import",
+ system: "System Computed"
+ }
+ },
+ unit: {
+ label: "Unit"
+ },
+ value_scale: {
+ label: "Decimal Places"
+ },
+ direction: {
+ label: "Direction",
+ options: {
+ positive: "Higher is Better",
+ negative: "Lower is Better"
+ }
+ },
+ scoring_method: {
+ label: "Scoring Method",
+ options: {
+ linear: "Linear",
+ step: "Step",
+ range: "Range Interpolation",
+ formula: "Custom Formula"
+ }
+ },
+ cap_rate: {
+ label: "Completion Cap (%)"
+ },
+ floor_rate: {
+ label: "Completion Floor (%)"
+ },
+ range_lower_rate: {
+ label: "Range Lower Completion (%)"
+ },
+ range_upper_rate: {
+ label: "Range Upper Completion (%)"
+ },
+ score_formula: {
+ label: "Scoring Formula"
+ },
+ description: {
+ label: "Definition Notes"
+ },
+ status: {
+ label: "Status",
+ options: {
+ active: "Active",
+ retired: "Inactive"
+ }
+ },
+ version_no: {
+ label: "Version"
+ },
+ superseded_by: {
+ label: "Superseded By"
+ }
+ },
+ _views: {
+ default: {
+ label: "All Indicators"
+ },
+ active: {
+ label: "Active Only"
+ }
+ },
+ _sections: {
+ basic: {
+ label: "Basic Information"
+ },
+ scoring: {
+ label: "Scoring Rule"
+ },
+ desc: {
+ label: "Definition Notes"
+ }
+ }
+ },
+ kpi_indicator_step: {
+ label: "Score Band",
+ pluralLabel: "Score Band",
+ description: "The completion-rate bands of the step scoring method and the score rate each yields.",
+ fields: {
+ indicator: {
+ label: "Indicator"
+ },
+ seq: {
+ label: "No."
+ },
+ label: {
+ label: "Band Name"
+ },
+ min_rate: {
+ label: "Completion Lower Bound (%, inclusive)"
+ },
+ max_rate: {
+ label: "Completion Upper Bound (%, exclusive; blank = no upper bound)"
+ },
+ score_rate: {
+ label: "Score Rate (%)"
+ }
+ },
+ _views: {
+ default: {
+ label: "Score Band"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Score Band"
+ }
+ }
+ },
+ kpi_personal_item: {
+ label: "Personal Item",
+ pluralLabel: "Personal Item",
+ description: "Specific indicators an employee carries personally, with weight and target.",
+ fields: {
+ assignment: {
+ label: "Staff Assignment"
+ },
+ name: {
+ label: "Item Name"
+ },
+ plan_indicator: {
+ label: "Assigned Indicator"
+ },
+ weight: {
+ label: "Item Weight (%)"
+ },
+ target_value: {
+ label: "Personal Target"
+ }
+ },
+ _views: {
+ default: {
+ label: "Personal Item"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Personal Item"
+ }
+ }
+ },
+ kpi_plan: {
+ label: "Assessment Plan",
+ pluralLabel: "Assessment Plans",
+ description: "The full configuration of one assessment cycle: period, indicator assignments, assessment relationships, staff assignments and the review workflow.",
+ fields: {
+ name: {
+ label: "Plan Name"
+ },
+ period_type: {
+ label: "Period Type",
+ options: {
+ month: "Month",
+ quarter: "Quarter",
+ half_year: "Half-Year",
+ year: "Year"
+ }
+ },
+ year: {
+ label: "Year"
+ },
+ period_no: {
+ label: "Period No. (month / quarter / half-year)"
+ },
+ period_start: {
+ label: "Period Start"
+ },
+ period_end: {
+ label: "Period End"
+ },
+ version_no: {
+ label: "Version"
+ },
+ status: {
+ label: "Status",
+ options: {
+ draft: "Draft",
+ published: "Published",
+ closed: "Closed",
+ archived: "Archived"
+ }
+ },
+ based_on: {
+ label: "Copy From Version"
+ },
+ published_at: {
+ label: "Published At"
+ },
+ published_by: {
+ label: "Published By"
+ },
+ description: {
+ label: "Plan Notes"
+ },
+ subject_count: {
+ label: "Participating Subjects"
+ },
+ indicator_count: {
+ label: "Indicators Assigned"
+ },
+ open_dispute_count: {
+ label: "Open Disputes"
+ }
+ },
+ _actions: {
+ kpi_plan_archive: {
+ label: "Archive Plan",
+ successMessage: "Plan archived."
+ },
+ kpi_plan_close: {
+ label: "Close Plan",
+ confirmText: "Once closed, no further entry or adjustment is accepted. Close this plan?",
+ successMessage: "Plan closed."
+ },
+ kpi_plan_publish: {
+ label: "Publish Plan",
+ confirmText: "An integrity check runs first. Once it passes, the plan freezes and an entry sheet is generated for every participating subject. Publish?",
+ successMessage: "Plan published; entry sheets have been generated."
+ }
+ },
+ _views: {
+ default: {
+ label: "All Plans"
+ },
+ draft: {
+ label: "Draft"
+ },
+ published: {
+ label: "Published"
+ },
+ board: {
+ label: "Status Board"
+ }
+ },
+ _sections: {
+ basic: {
+ label: "Plan Information"
+ },
+ publish: {
+ label: "Publication"
+ }
+ }
+ },
+ kpi_plan_indicator: {
+ label: "Indicator Assignment",
+ pluralLabel: "Indicator Assignment",
+ description: "Assigns an indicator to a subject with a weight and a target value.",
+ fields: {
+ plan: {
+ label: "Plan"
+ },
+ name: {
+ label: "Assignment Name"
+ },
+ indicator: {
+ label: "Indicator"
+ },
+ subject: {
+ label: "Subject"
+ },
+ weight: {
+ label: "Weight (%)"
+ },
+ target_value: {
+ label: "Target Value"
+ },
+ baseline_value: {
+ label: "Baseline (Previous Period)"
+ },
+ data_source: {
+ label: "Data Source (overrides indicator default)",
+ options: {
+ inherit: "Inherit from Indicator",
+ manual: "Manual Entry",
+ import: "Bulk Import",
+ system: "System Computed"
+ }
+ },
+ dispute_status: {
+ label: "Dispute Status",
+ options: {
+ none: "No Dispute",
+ open: "In Dispute",
+ resolved: "Resolved"
+ }
+ },
+ remark: {
+ label: "Remark"
+ }
+ },
+ _views: {
+ default: {
+ label: "Indicator Assignment"
+ },
+ disputed: {
+ label: "In Dispute"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Indicator Assignment"
+ }
+ }
+ },
+ kpi_plan_step: {
+ label: "Workflow Step",
+ pluralLabel: "Workflow Step",
+ description: "The order of review steps in a plan and the position that reviews each.",
+ fields: {
+ plan: {
+ label: "Plan"
+ },
+ seq: {
+ label: "Order"
+ },
+ step_type: {
+ label: "Step Type",
+ options: {
+ dept_submit: "Department Entry",
+ branch_check: "Branch Verification (Parallel)",
+ hr_review: "HR Review",
+ leader_approve: "Executive Approval"
+ }
+ },
+ label: {
+ label: "Step Name"
+ },
+ approver_position: {
+ label: "Review Position",
+ options: {
+ kpi_dept_reporter: "Department Reporter",
+ kpi_branch_checker: "Branch Checker",
+ kpi_hr_reviewer: "HR Review",
+ kpi_hr_head: "HR Head",
+ kpi_exec_leader: "Executive Leader"
+ }
+ }
+ },
+ _views: {
+ default: {
+ label: "Workflow Step"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Workflow Step"
+ }
+ }
+ },
+ kpi_plan_subject: {
+ label: "Participating Subject",
+ pluralLabel: "Participating Subject",
+ description: "A plan's assessed subjects, owning departments, subject weights and executive leaders.",
+ fields: {
+ plan: {
+ label: "Plan"
+ },
+ name: {
+ label: "Subject Name"
+ },
+ subject: {
+ label: "Subject"
+ },
+ subject_type: {
+ label: "Subject Type",
+ options: {
+ department: "HQ Department",
+ branch: "Branch"
+ }
+ },
+ assessor_unit: {
+ label: "Owning Department (sets the indicators)"
+ },
+ subject_weight: {
+ label: "Subject Weight (%)"
+ },
+ leader: {
+ label: "Executive Leader"
+ },
+ remark: {
+ label: "Remark"
+ }
+ },
+ _views: {
+ default: {
+ label: "Participating Subject"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Participating Subject"
+ }
+ }
+ },
+ kpi_result: {
+ label: "Assessment Result",
+ pluralLabel: "Assessment Results",
+ description: "Scores and rankings aggregated across four dimensions: department, branch, person and executive leader.",
+ fields: {
+ name: {
+ label: "Result Name"
+ },
+ plan: {
+ label: "Assessment Plan"
+ },
+ dimension: {
+ label: "Dimension",
+ options: {
+ department: "Department",
+ branch: "Branch",
+ person: "Person",
+ leader: "Executive Leader"
+ }
+ },
+ unit: {
+ label: "Org Unit"
+ },
+ person: {
+ label: "Person"
+ },
+ sheet: {
+ label: "Source Entry Sheet"
+ },
+ score: {
+ label: "Score"
+ },
+ weighted_score: {
+ label: "Weighted Score"
+ },
+ rank: {
+ label: "Rank"
+ },
+ breakdown: {
+ label: "Calculation Breakdown"
+ },
+ generated_at: {
+ label: "Generated At"
+ }
+ },
+ _views: {
+ default: {
+ label: "Assessment Result"
+ },
+ department: {
+ label: "Department"
+ },
+ branch: {
+ label: "Branch"
+ },
+ person: {
+ label: "Person"
+ },
+ leader: {
+ label: "Executive Leader"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Assessment Result"
+ }
+ }
+ },
+ kpi_review_record: {
+ label: "Review Record",
+ pluralLabel: "Review Record",
+ description: "An append-only trace of every workflow action on an entry sheet: who, when, from which status to which, and why.",
+ fields: {
+ name: {
+ label: "Record No."
+ },
+ sheet: {
+ label: "Entry Sheet"
+ },
+ action: {
+ label: "Action",
+ options: {
+ generate: "Generated",
+ submit: "Submit",
+ confirm: "Confirm",
+ dispute: "Raise Dispute",
+ approve: "Approve",
+ reject: "Reject",
+ archive: "Archive",
+ adjust: "Data Adjustment"
+ }
+ },
+ step_label: {
+ label: "Step"
+ },
+ from_status: {
+ label: "From Status"
+ },
+ to_status: {
+ label: "To Status"
+ },
+ actor: {
+ label: "Actor"
+ },
+ reason: {
+ label: "Reason / Comment"
+ },
+ acted_at: {
+ label: "Time"
+ }
+ },
+ _views: {
+ default: {
+ label: "Review Record"
+ },
+ rejects: {
+ label: "Rejections"
+ },
+ timeline: {
+ label: "Timeline"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Review Record"
+ }
+ }
+ },
+ kpi_snapshot: {
+ label: "Archive Snapshot",
+ pluralLabel: "Archive Snapshots",
+ description: "The immutable record of an archived entry sheet — the authority for historical queries.",
+ fields: {
+ name: {
+ label: "Snapshot Name"
+ },
+ plan: {
+ label: "Assessment Plan"
+ },
+ sheet: {
+ label: "Entry Sheet"
+ },
+ subject: {
+ label: "Subject"
+ },
+ total_score: {
+ label: "Final Score"
+ },
+ payload: {
+ label: "Snapshot Payload"
+ },
+ checksum: {
+ label: "Checksum"
+ },
+ archived_by: {
+ label: "Archived By"
+ },
+ archived_at: {
+ label: "Archived At"
+ }
+ },
+ _views: {
+ default: {
+ label: "Archive Snapshot"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Archive Snapshot"
+ }
+ }
+ },
+ kpi_staff_assignment: {
+ label: "Staff Assignment",
+ pluralLabel: "Staff Assignment",
+ description: "Which department segments an employee carries, with the weight and personal coefficient for each.",
+ fields: {
+ plan: {
+ label: "Plan"
+ },
+ name: {
+ label: "Staff Assignment Name"
+ },
+ employee: {
+ label: "Employee"
+ },
+ unit: {
+ label: "Department Segment"
+ },
+ weight: {
+ label: "Assignment Weight (%)"
+ },
+ coefficient: {
+ label: "Personal Coefficient"
+ },
+ remark: {
+ label: "Remark"
+ }
+ },
+ _views: {
+ default: {
+ label: "Staff Assignment"
+ }
+ },
+ _sections: {
+ main: {
+ label: "Staff Assignment"
+ }
+ }
+ }
+ },
+ apps: {
+ kpi_app: {
+ label: "KPI Assessment",
+ navigation: {
+ nav_home: {
+ label: "Workbench"
+ },
+ group_entry: {
+ label: "Entry & Review"
+ },
+ nav_sheets: {
+ label: "Entry Sheet"
+ },
+ nav_my_checks: {
+ label: "My Verifications"
+ },
+ nav_hr_queue: {
+ label: "Awaiting HR Review"
+ },
+ nav_leader_queue: {
+ label: "Awaiting Executive Approval"
+ },
+ nav_lines: {
+ label: "Entry Line"
+ },
+ nav_checks: {
+ label: "Branch Verification"
+ },
+ nav_bonus: {
+ label: "Bonus & Penalty"
+ },
+ nav_adjust: {
+ label: "Data Adjustment"
+ },
+ group_plan: {
+ label: "Assessment Plan"
+ },
+ nav_plans: {
+ label: "Plan Version"
+ },
+ nav_plan_indicators: {
+ label: "Indicator Assignment"
+ },
+ nav_plan_subjects: {
+ label: "Participating Subject"
+ },
+ nav_assignments: {
+ label: "Staff Assignment"
+ },
+ nav_disputes: {
+ label: "Indicator Dispute"
+ },
+ group_results: {
+ label: "Results & Archive"
+ },
+ nav_dashboard: {
+ label: "Results Board"
+ },
+ nav_results: {
+ label: "Assessment Result"
+ },
+ nav_report_unit: {
+ label: "Department × Dimension Matrix"
+ },
+ nav_report_person: {
+ label: "Person Scores"
+ },
+ nav_report_line: {
+ label: "Indicator Completion"
+ },
+ nav_snapshots: {
+ label: "Archive Snapshot"
+ },
+ group_setup: {
+ label: "Setup"
+ },
+ nav_indicators: {
+ label: "Indicator Library"
+ },
+ nav_ops_dashboard: {
+ label: "Workflow Progress"
+ },
+ group_audit: {
+ label: "Audit"
+ },
+ nav_reviews: {
+ label: "Review Record"
+ }
+ }
+ }
+ },
+ dashboards: {
+ kpi_progress_dashboard: {
+ label: "Assessment Progress",
+ description: "Where each entry sheet sits in the workflow, the spread of subject types, and average indicator completion.",
+ widgets: {
+ p_draft: {
+ title: "Entering"
+ },
+ p_check: {
+ title: "Under Branch Verification"
+ },
+ p_review: {
+ title: "In Review"
+ },
+ p_done: {
+ title: "Passed / Archived"
+ },
+ p_status: {
+ title: "Entry Sheets by Status"
+ },
+ p_type: {
+ title: "Average Indicator Score by Subject Type"
+ },
+ p_completion: {
+ title: "Average Completion by Indicator (%)"
+ }
+ }
+ },
+ kpi_results_dashboard: {
+ label: "Assessment Results Board",
+ description: "An overview of scores across the department, branch, person and executive-leader dimensions, drillable to the detail.",
+ widgets: {
+ m_count: {
+ title: "Results"
+ },
+ m_avg: {
+ title: "Average Score"
+ },
+ m_max: {
+ title: "Highest Score"
+ },
+ m_min: {
+ title: "Lowest Score"
+ },
+ bar_unit: {
+ title: "Average Score by Org Unit (Department / Branch)"
+ },
+ bar_person: {
+ title: "Person Score"
+ },
+ tbl_dim: {
+ title: "By Dimension"
+ }
+ }
+ }
+ },
+ pages: {
+ kpi_home: {
+ label: "Workbench",
+ title: "KPI Assessment",
+ subtitle: "Indicator library → plan configuration → indicator assignment → entry → parallel verification → review → scoring → adjustment → aggregation → archive"
+ }
+ }
+};
diff --git a/src/translations/index.ts b/src/translations/index.ts
index b157982..08c53df 100644
--- a/src/translations/index.ts
+++ b/src/translations/index.ts
@@ -1,25 +1,23 @@
import { defineTranslationBundle } from '@objectstack/spec';
import { zhCNTranslations } from './zh-CN.objects.generated.js';
+import { enTranslations } from './en.objects.generated.js';
/**
- * 翻译包按平台正式写法组织:每个受支持语言各一个包。
+ * 翻译包按平台正式写法组织:每个受支持语言各一个包,均由
+ * `pnpm i18n:extract`(`os i18n extract --locales=zh-CN,en --fill=default`)生成。
*
- * - `zh-CN`(默认语言)由 `pnpm i18n:extract` 从元数据中文 label 生成,
- * 全部词条为工具产出,禁止手改;元数据 label 改动后重跑生成,
- * `pnpm verify` 里的 `extract --check` 新鲜度门禁负责兜底。
- * - `en` 手工维护,只为英文界面用户提供英文回退。
+ * - `zh-CN`(默认语言)的叶子由工具从元数据中文 label 直接填充,**全部词条禁止手改**;
+ * 元数据 label 改动后重跑生成即可。
+ * - `en` 的叶子由 `--fill=default` 先填入中文源串,再**逐条人工翻译**;
+ * 重跑生成时 merge 只补空缺(present-but-stale 不算空缺),已译词条原样保留 ——
+ * 所以元数据 label 改了之后,对应的英文词条要人工重译,工具不会提醒。
+ *
+ * 两个包都由 `pnpm verify` 里的 `extract --check` 守新鲜度:少了键就失败。
+ *
+ * 覆盖范围是元数据词条(objects / apps / dashboards / pages)。视图页签、动作按钮文案与
+ * hook 抛出的三段式报错**不在**翻译包内 —— 它们写在各自的定义与代码里,目前只有中文。
*/
export const KpiTranslationBundle = defineTranslationBundle({
'zh-CN': zhCNTranslations,
- en: {
- objects: {
- kpi_indicator: { label: 'Indicator', pluralLabel: 'Indicators', fields: { code: { label: 'Code' }, name: { label: 'Name' }, category: { label: 'Category' }, owner_unit: { label: 'Department Segment' }, scoring_method: { label: 'Scoring Method' }, status: { label: 'Status' } } },
- kpi_plan: { label: 'Assessment Plan', pluralLabel: 'Assessment Plans', fields: { name: { label: 'Plan Name' }, period_type: { label: 'Period Type' }, year: { label: 'Year' }, status: { label: 'Status' } } },
- kpi_entry_sheet: { label: 'Entry Sheet', pluralLabel: 'Entry Sheets', fields: { name: { label: 'Sheet' }, plan: { label: 'Plan' }, subject: { label: 'Subject' }, status: { label: 'Status' }, total_score: { label: 'Final Score' } } },
- kpi_entry_line: { label: 'Entry Line', pluralLabel: 'Entry Lines', fields: { indicator_name: { label: 'Indicator' }, actual_value: { label: 'Actual' }, target_value: { label: 'Target' }, weight: { label: 'Weight (%)' }, completion_rate: { label: 'Completion (%)' }, score: { label: 'Score' }, final_score: { label: 'Final Score' } } },
- kpi_result: { label: 'Assessment Result', pluralLabel: 'Assessment Results', fields: { dimension: { label: 'Dimension' }, score: { label: 'Score' }, rank: { label: 'Rank' } } },
- kpi_snapshot: { label: 'Archive Snapshot', pluralLabel: 'Archive Snapshots' },
- },
- apps: { kpi_app: { label: 'KPI Assessment' } },
- },
+ en: enTranslations,
});