Skip to content

refactor: move runtime implementation out of spec protocol package#856

Merged
hotlong merged 4 commits intomainfrom
copilot/add-object-field-validation
Mar 2, 2026
Merged

refactor: move runtime implementation out of spec protocol package#856
hotlong merged 4 commits intomainfrom
copilot/add-object-field-validation

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 2, 2026

packages/spec is the protocol package — it should contain only definitions (Zod schemas, TypeScript types, constants). The generateTranslationSkeleton() and validateTranslationCompleteness() runtime functions added in the previous PR violate the "no business logic" principle.

Removed from spec

  • translation-skeleton.ts — stripped generateTranslationSkeleton() function; retained TRANSLATE_PLACEHOLDER constant (protocol convention)
  • translation-validator.ts — deleted entirely (validateTranslationCompleteness(), ValidationResult, and all helpers)
  • Test files for the two removed functions (17 tests)
  • index.ts export of translation-validator

Retained in spec (definitions only)

  • translation-typegen.ts — pure types (StrictObjectTranslation<T>) ✅
  • TRANSLATE_PLACEHOLDER constant — protocol-level skeleton marker ✅
  • translation.zod.ts — all Zod schemas unchanged ✅
  • Example zh-CN.ts satisfies pattern + completeness test — depend only on types ✅

Runtime implementations should be re-added in CLI (objectstack i18n extract), service-i18n, or metadata packages.

What stays in spec after this change

// translation-typegen.ts — pure type utility
export type StrictObjectTranslation<Obj extends { fields: Record<string, unknown> }> = {
  label: string;
  pluralLabel?: string;
  fields: StrictFieldTranslations<Obj['fields']>;
};

// translation-skeleton.ts — protocol constant only
export const TRANSLATE_PLACEHOLDER = '__TRANSLATE__';
Original prompt

This section details on the original issue you should resolve

<issue_title>为翻译准确性开发“对象字段/选项完整性约束”与AI友好翻译骨架工具</issue_title>
<issue_description>## 背景

当前翻译文件(如 examples/app-todo/src/translations/zh-CN.ts)使用 TranslationData 类型,底层是 z.record(z.string(), FieldTranslationSchema) —— 只验证值的形状,不验证 key 的完整性

这意味着:

  • AI 生成翻译时漏了 5 个字段,TypeScript 不报错,Zod 也不报错
  • 多写了一个不存在的字段 key,也不报错
  • select/multiselect 的 option 翻译少了几个,同样静默通过

参考现状:

  • 对象定义 task.object.ts 有 18 个字段,3 个 select 共 14 个 option
  • 翻译文件 zh-CN.ts 只有 TranslationData 松散类型约束

目标

建立三重防线,保证 AI/人工翻译 字段全覆盖、option 不缺不漏、格式严格合规

Object 定义 (task.object.ts)
    │
    ├──→ ① TypeScript satisfies 约束    ← tsc 编译报错(少/多字段)
    │
    ├──→ ② AI 翻译骨架 JSON(填空题)    ← 结构已锁死,AI 不可能加减 key
    │
    └──→ ③ Vitest 完整性测试            ← CI 自动拦截

开发任务

Task 1: 类型工具 translation-typegen.ts

新增文件: packages/spec/src/system/translation-typegen.ts

从 Object 定义自动推导严格翻译类型:

// 核心泛型:把 fields Record 的 key 提取出来,要求翻译必须覆盖每一个
type StrictFieldTranslations<Fields> = {
  [K in keyof Fields]-?:    // -? = 去掉可选,变成必填
    Fields[K] extends { options: ... }
      ? { label: string; options: Record<OptionValue, string> }  // select 字段:options 也必填
      : { label: string; help?: string; placeholder?: string }   // 普通字段:label 必填
};

type StrictObjectTranslation<Obj> = {
  label: string;
  pluralLabel?: string;
  fields: StrictFieldTranslations<Obj['fields']>;
};

验收标准:

  • StrictObjectTranslation<typeof Task> 可以正确推导出 18 个字段 key 为必填
  • select 字段(status/priority/tags)的 option value 全部成为 options 的必填 key
  • 少字段 → TS2741 报错;多字段 → TS2353 报错;少 option → TS2741 报错
  • packages/spec/src/system/index.ts 导出
  • 有单元测试验证类型推导正确性(利用 expectTypeOf@ts-expect-error

Task 2: 翻译骨架生成器 translation-skeleton.ts

新增文件: packages/spec/src/system/translation-skeleton.ts

从 Object 定义自动生成 AI 友好的 JSON 填空模板:

function generateTranslationSkeleton(objectDef: ServiceObject): string

输出示例(task 对象):

{
  "label": "__TRANSLATE__: \"Task\"",
  "pluralLabel": "__TRANSLATE__: \"Tasks\"",
  "fields": {
    "subject": { "label": "__TRANSLATE__: \"Subject\"" },
    "status": {
      "label": "__TRANSLATE__: \"Status\"",
      "options": {
        "not_started": "__TRANSLATE__: \"Not Started\"",
        "in_progress": "__TRANSLATE__: \"In Progress\"",
        "waiting": "__TRANSLATE__: \"Waiting\"",
        "completed": "__TRANSLATE__: \"Completed\"",
        "deferred": "__TRANSLATE__: \"Deferred\""
      }
    }
  }
}

验收标准:

  • 输入 task.object.ts 的对象定义 → 输出包含全部 18 个字段的骨架
  • select 字段自动提取所有 option value 并生成 options 映射
  • description 的字段自动生成 help 占位符
  • 输出是合法 JSON,可直接被 ObjectTranslationNodeSchema.parse() 验证(替换占位符后)
  • 有单元测试

Task 3: 完整性校验函数 validateTranslationCompleteness()

新增文件: packages/spec/src/system/translation-validator.ts

function validateTranslationCompleteness(
  objectDef: ServiceObject,
  translation: unknown,
): { valid: boolean; errors: string[] }

校验维度:

  1. Zod 结构验证ObjectTranslationNodeSchema.safeParse()
  2. 字段完整性 — 源 fields 中有但翻译中没有 → 报 缺失字段
  3. 字段多余性 — 翻译中有但源 fields 中没有 → 报 多余字段
  4. Option 完整性 — select 字段的每个 option value 都必须有翻译
  5. 残留检查 — 不允许 __TRANSLATE__ 占位符残留

验收标准:

  • 缺 1 个字段 → errors 包含该字段名
  • 多 1 个字段 → errors 包含该字段名
  • 缺 1 个 option → errors 包含 fields.{name}.options.{value}
  • __TRANSLATE__ 残留 → errors 报告
  • 有完整单元测试

Task 4: 改造 Todo 示例作为范例

修改文件: examples/app-todo/src/translations/zh-CN.ts

// Before:
import type { TranslationData } from '@objectstack/spec/system';
export const zhCN: TranslationData = { ... };

// After:
import type { StrictObjectTranslation } from '@objectstack/spec/system';
import { Task } from '../objects/task.object';
type TaskTranslation = StrictObjectTranslation<typeof Task>;

export const zhCN = {
  objects: {
    task: { ... } satisfies TaskTranslation,
  },
  // ...
};

验收标准:

  • tsc --noEmit 编译通过
  • 故意删掉一个字段 → tsc 报错(在测试中用 @ts-expect-error 验证)

Task 5: Vitest 完整性测试

新增文件: examples/app-todo/src/translations/translation-completeness.test.ts

const fieldNames = Object.keys(Task.fields);
const selectFields = Object.entries(Task.fields)
  .filter(([_, f]) => Array.isArray(f.options))
  .map(([name, f]) => ({ name, values: f.options.map(o => o.value) }));

describe.each([['en', en], ['zh-CN', zhCN]])('%s', (locale, t) => {
  it.each(fieldNames)('field: %s', (name) => {
    expect(t.objects?.task?.fields?.[name]?.label).toBeTruthy();
  });
  it.each(selectFields)('options: $name', ({ name, values }) => {
    for (const v of values) {
      expect(t.objects?.task?.fields?.[name]?.options?.[v]).toBeTruthy();
   ...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes objectstack-ai/spec#855

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Error Error Mar 2, 2026 5:14am
spec Error Error Mar 2, 2026 5:14am

Request Review

Copilot AI and others added 2 commits March 2, 2026 03:52
…tor)

- Add StrictObjectTranslation type utility for compile-time field/option enforcement
- Add generateTranslationSkeleton() for AI-friendly translation templates
- Add validateTranslationCompleteness() for runtime completeness validation
- Make ObjectSchema.create() generic to preserve field key types
- Update zh-CN/en/ja-JP translations with missing category and recurrence_type options
- Add satisfies pattern to zh-CN.ts as reference example
- Add comprehensive unit tests (27 new tests in spec, 52 in example)

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
- Simplify ExtractOptionValues to use only ReadonlyArray (supertype of Array)
- Rename ambiguous 'obj' parameter to 'value' in checkPlaceholderResidue
- Use Omit<ServiceObject, 'fields'> & Pick<T, 'fields'> to avoid type conflicts

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add object field integrity constraints for translation accuracy feat: translation completeness tooling — strict types, skeleton generator, runtime validator Mar 2, 2026
Move generateTranslationSkeleton() and validateTranslationCompleteness()
out of packages/spec per spec's "no business logic" principle.

Spec retains only protocol definitions:
- StrictObjectTranslation type utility (translation-typegen.ts)
- TRANSLATE_PLACEHOLDER constant (translation-skeleton.ts)
- All existing Zod schemas (translation.zod.ts)

Runtime implementations should be re-implemented in appropriate
packages (CLI, service-i18n, metadata) in future work.

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title feat: translation completeness tooling — strict types, skeleton generator, runtime validator refactor: move runtime implementation out of spec protocol package Mar 2, 2026
@hotlong hotlong marked this pull request as ready for review March 2, 2026 05:32
Copilot AI review requested due to automatic review settings March 2, 2026 05:32
@hotlong hotlong merged commit 2e6a945 into main Mar 2, 2026
2 of 4 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the spec protocol package to keep it definition-only by removing i18n runtime implementations, while retaining protocol-level constants and type utilities for compile-time translation completeness checks. Updates the Todo example translations accordingly and adds a completeness test.

Changes:

  • Adds StrictObjectTranslation<T> type utility (and tests) for compile-time enforcement of field/option translation completeness.
  • Strips translation skeleton functionality down to the protocol constant TRANSLATE_PLACEHOLDER and exports new system modules.
  • Updates Todo example translations to include missing select option maps and adds a translation completeness test.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/spec/src/system/translation-typegen.ts Introduces strict translation types (type-level only).
packages/spec/src/system/translation-typegen.test.ts Adds tests for the new type utility (but currently relies on typechecking).
packages/spec/src/system/translation-skeleton.ts Keeps only the protocol placeholder constant.
packages/spec/src/system/index.ts Exports the new typegen and skeleton constant modules.
packages/spec/src/data/object.zod.ts Adjusts ObjectSchema.create typing to preserve literal field keys for type-level tooling.
examples/app-todo/src/translations/zh-CN.ts Applies satisfies StrictObjectTranslation<typeof Task> and fills missing select options.
examples/app-todo/src/translations/en.ts Adds missing select option translations (category, recurrence_type).
examples/app-todo/src/translations/ja-JP.ts Adds missing select option translations (category, recurrence_type).
examples/app-todo/src/translations/translation-completeness.test.ts Adds a locale translation completeness test (currently Vitest-based).
Comments suppressed due to low confidence (1)

packages/spec/src/system/translation-typegen.test.ts:110

  • This test file uses expectTypeOf and @ts-expect-error to validate type-level behavior, but packages/spec runs vitest run without any typechecking step (Vitest transpiles via esbuild and won’t enforce these assertions). Add a typecheck phase (e.g., vitest --typecheck or tsc --noEmit in CI/scripts) so these type-safety tests actually fail when the types regress.
  it('should report TS error when a field is missing', () => {
    type T = StrictObjectTranslation<typeof SimpleObject>;
    // @ts-expect-error — missing 'email' field
    const _invalid: T = {
      label: 'Test',
      fields: {
        name: { label: 'Name' },
      },
    };
    expect(_invalid).toBeDefined();
  });

Comment on lines +3 to +8
import { describe, it, expect } from 'vitest';
import { Task } from '../objects/task.object';
import { en } from './en';
import { zhCN } from './zh-CN';
import type { TranslationData } from '@objectstack/spec/system';

Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

examples/app-todo imports vitest here, but the app package does not declare vitest in devDependencies and its test script runs objectstack test (QA runner), not Vitest. Also tsconfig.json includes src/**/*, so pnpm typecheck will fail with “Cannot find module 'vitest'”. Either add Vitest (and a Vitest-based test script/config) to this package, or move this check into the existing QA/Node test harness so it actually runs.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants