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. -![Workbench](docs/手册/图片/01-02-gongzuotai.png) +![One assessment cycle, by role](docs/readme/flow-swimlane.png) + +*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 in the console](docs/readme/ui-entry-sheets.png) + +*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 -``` +![Entry sheet state machine and its gates](docs/readme/state-machine.png) - **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, });