Skip to content

Commit 878c1ed

Browse files
os-zhuangclaude
andauthored
fix(cli): hide platform i18n baseline from os lint by default (#3102)
A fresh create-objectstack project reported 800+ errors out of the box, every one i18n/missing-metadataForm — translation keys for platform built-in metadata forms collected from the static registries (DEFAULT_METADATA_TYPE_REGISTRY + METADATA_FORM_REGISTRY). The platform packages already ship those translations at runtime, so for a user project they are pure noise that drowns the user's own i18n signal (15.1 third-party eval, P2). - fold i18n coverage through foldCoverageIssues(): metadataForm-source issues are hidden by default and counted - new --include-platform flag surfaces them for a full audit - human output gains a dim summary line; --json gains hiddenPlatform - user-authored coverage (object/field/view/action) reported unchanged Verified against a real scaffold: stock CLI 821 errors -> patched CLI 4 user-signal errors + 'platform built-ins: 817 i18n issue(s) hidden', --include-platform restores 821, --json reports hiddenPlatform: 817. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 809214f commit 878c1ed

3 files changed

Lines changed: 121 additions & 9 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os lint` no longer buries the user's own signal under the platform i18n baseline. A fresh scaffold reported 800+ `i18n/missing-metadataForm` errors — translation keys for platform built-in metadata forms (email_template, …) that the platform packages already ship at runtime. Those are now hidden by default and folded into one summary line (`platform built-ins: N i18n issue(s) hidden`); pass `--include-platform` to audit them, and read `hiddenPlatform` in `--json` output. User-authored metadata coverage is reported unchanged.

packages/cli/src/commands/lint.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { bundleRequire } from 'bundle-require';
66
import { normalizeStackInput } from '@objectstack/spec';
77
import { PROTOCOL_MAJOR } from '@objectstack/spec/kernel';
88
import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
9-
import { computeI18nCoverage } from '../utils/i18n-coverage.js';
9+
import { computeI18nCoverage, type CoverageIssue } from '../utils/i18n-coverage.js';
1010
import { lintDataModel } from '../lint/data-model-rules.js';
1111
import { validateWidgetBindings } from '@objectstack/lint';
1212
import { validateRecordTitle, validateSemanticRoles, validateCapabilityReferences, validateSecurityPosture, validateApprovalApprovers } from '@objectstack/lint';
@@ -36,6 +36,34 @@ interface LintIssue {
3636
fix?: string;
3737
}
3838

39+
// Fold i18n coverage issues into lint issues, separating the platform
40+
// baseline from the user's own metadata. `metadataForm` issues come from
41+
// walking the static platform registries (DEFAULT_METADATA_TYPE_REGISTRY +
42+
// METADATA_FORM_REGISTRY) — ~850 Studio-form keys that the platform packages
43+
// already translate at runtime. On a fresh project they would drown every
44+
// user-authored signal (15.1 third-party eval: 848/848 errors were platform
45+
// noise), so they are hidden unless explicitly requested.
46+
export function foldCoverageIssues(
47+
coverageIssues: CoverageIssue[],
48+
includePlatform: boolean,
49+
): { folded: LintIssue[]; hiddenPlatform: number } {
50+
const folded: LintIssue[] = [];
51+
let hiddenPlatform = 0;
52+
for (const c of coverageIssues) {
53+
if (!includePlatform && c.source === 'metadataForm') {
54+
hiddenPlatform++;
55+
continue;
56+
}
57+
folded.push({
58+
severity: c.severity === 'error' ? 'error' : 'warning',
59+
rule: `i18n/missing-${c.source}`,
60+
message: c.message,
61+
path: `translations.${c.locale}.${c.key}`,
62+
});
63+
}
64+
return { folded, hiddenPlatform };
65+
}
66+
3967
// ─── Rules ──────────────────────────────────────────────────────────
4068

4169
const SNAKE_CASE_RE = /^[a-z][a-z0-9_]*$/;
@@ -441,6 +469,10 @@ export default class Lint extends Command {
441469
default: 75,
442470
}),
443471
'skip-i18n': Flags.boolean({ description: 'Skip translation coverage checks' }),
472+
'include-platform': Flags.boolean({
473+
description:
474+
'Also report i18n coverage for platform built-in metadata forms (hidden by default — the platform packages ship those translations)',
475+
}),
444476
'i18n-strict': Flags.boolean({
445477
description: 'Treat missing translations in non-default locales as errors',
446478
}),
@@ -486,19 +518,18 @@ export default class Lint extends Command {
486518
}
487519

488520
// ── Translation coverage ──
521+
let hiddenPlatform = 0;
489522
if (!flags['skip-i18n']) {
490523
const coverage = computeI18nCoverage(normalized, {
491524
defaultLocale: flags['default-locale'],
492525
strict: flags['i18n-strict'],
493526
});
494-
for (const c of coverage.issues) {
495-
issues.push({
496-
severity: c.severity === 'error' ? 'error' : 'warning',
497-
rule: `i18n/missing-${c.source}`,
498-
message: c.message,
499-
path: `translations.${c.locale}.${c.key}`,
500-
});
501-
}
527+
const { folded, hiddenPlatform: hidden } = foldCoverageIssues(
528+
coverage.issues,
529+
flags['include-platform'] ?? false,
530+
);
531+
hiddenPlatform = hidden;
532+
issues.push(...folded);
502533
}
503534

504535
// Metadata-quality score (the lint rubric expressed as 0–100).
@@ -515,6 +546,7 @@ export default class Lint extends Command {
515546
errors: errors.length,
516547
warnings: warnings.length,
517548
suggestions: suggestions.length,
549+
...(hiddenPlatform > 0 ? { hiddenPlatform } : {}),
518550
...(score ? { score: score.score, grade: score.grade } : {}),
519551
issues,
520552
duration: timer.elapsed(),
@@ -525,8 +557,19 @@ export default class Lint extends Command {
525557

526558
console.log('');
527559

560+
const printHiddenPlatform = () => {
561+
if (hiddenPlatform > 0) {
562+
console.log(
563+
chalk.dim(
564+
` platform built-ins: ${hiddenPlatform} i18n issue(s) hidden — rerun with --include-platform to audit them`,
565+
),
566+
);
567+
}
568+
};
569+
528570
if (issues.length === 0) {
529571
printSuccess(`All checks passed ${chalk.dim(`(${timer.display()})`)}`);
572+
printHiddenPlatform();
530573
if (score) this.printScore(score);
531574
console.log('');
532575
return;
@@ -578,6 +621,7 @@ export default class Lint extends Command {
578621
if (warnings.length > 0) parts.push(chalk.yellow(`${warnings.length} warning(s)`));
579622
if (suggestions.length > 0) parts.push(chalk.blue(`${suggestions.length} suggestion(s)`));
580623
console.log(` ${parts.join(', ')} ${chalk.dim(`(${timer.display()})`)}`);
624+
printHiddenPlatform();
581625

582626
if (score) this.printScore(score);
583627

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license.
2+
//
3+
// `os lint` platform-noise fold (15.1 third-party eval): a fresh scaffold
4+
// reported 848 errors, all `i18n/missing-metadataForm` — keys expected by the
5+
// static platform registries and already translated by the platform packages
6+
// at runtime. They must not drown the user's own i18n signal: hidden by
7+
// default, surfaced only under --include-platform, always counted.
8+
9+
import { describe, it, expect } from 'vitest';
10+
import { foldCoverageIssues } from '../src/commands/lint';
11+
import type { CoverageIssue } from '../src/utils/i18n-coverage';
12+
13+
const userIssue: CoverageIssue = {
14+
severity: 'error',
15+
locale: 'en',
16+
key: 'objects.blank_note.label',
17+
message: "Missing en translation for object 'blank_note' label",
18+
source: 'object',
19+
};
20+
21+
const platformIssue: CoverageIssue = {
22+
severity: 'error',
23+
locale: 'en',
24+
key: 'metadataForms.email_template.fields.subject.label',
25+
message: 'Missing en translation for metadata form email_template',
26+
source: 'metadataForm',
27+
};
28+
29+
describe('foldCoverageIssues', () => {
30+
it('hides platform metadata-form issues by default and counts them', () => {
31+
const { folded, hiddenPlatform } = foldCoverageIssues(
32+
[userIssue, platformIssue, { ...platformIssue, locale: 'zh-CN', severity: 'warning' }],
33+
false,
34+
);
35+
expect(hiddenPlatform).toBe(2);
36+
expect(folded).toHaveLength(1);
37+
expect(folded[0]).toMatchObject({
38+
severity: 'error',
39+
rule: 'i18n/missing-object',
40+
path: 'translations.en.objects.blank_note.label',
41+
});
42+
});
43+
44+
it('keeps the user signal intact when nothing is platform-sourced', () => {
45+
const { folded, hiddenPlatform } = foldCoverageIssues([userIssue], false);
46+
expect(hiddenPlatform).toBe(0);
47+
expect(folded).toHaveLength(1);
48+
});
49+
50+
it('surfaces everything under --include-platform', () => {
51+
const { folded, hiddenPlatform } = foldCoverageIssues(
52+
[userIssue, platformIssue],
53+
true,
54+
);
55+
expect(hiddenPlatform).toBe(0);
56+
expect(folded).toHaveLength(2);
57+
expect(folded.map((i) => i.rule).sort()).toEqual([
58+
'i18n/missing-metadataForm',
59+
'i18n/missing-object',
60+
]);
61+
expect(folded[1].severity).toBe('error');
62+
});
63+
});

0 commit comments

Comments
 (0)