Skip to content

Commit 1252db9

Browse files
fix(cli): os i18n check reads inlineHelpText as the default-locale source of a field help key (#25720)
Fixes #25683 Clause-②: no Metadata-protocol: none ## What changed The inline source of a field's help key (`objects.OBJECT.fields.FIELD.help`) in `packages/cli/src/utils/i18n-extract.ts` is now `field.help ?? field.inlineHelpText ?? field.description`, per the triage route: `inlineHelpText` is the default-locale source, after `help` and before `description`, the same way an authored label is (#15711, ruled A). A field that authors `inlineHelpText` and translates `help` into one locale no longer has its default locale reported as missing; a non-default locale with no `help` is still reported. ## Mechanism measured That call is the only site where the field help key takes default-locale text (`git grep` for `help ??` in `packages/**/src` finds only it). Other help-key sites, each with a verdict: - Flow screen fields (`flows.FLOW.screens.NODE.fields.FIELD.inlineHelpText`): already read `inlineHelpText` directly, no fallback chain. No change. - Form section entries (`_sections.SECTION.fields.FIELD.helpText`): read the entry's own `helpText` only, no `help ?? description`. No change. - Bulk action params (`params.PARAM.help`) and action params (`params.PARAM.helpText`): read their own single key. No change. - `metadataForms.TYPE.fields.PATH.helpText`: own single key. No change. ## Landing differs from the claim's file surface (one file added) The claim named the extractor, its tests and one changeset. The change makes the showcase example's `contact.lead_score` field (it authors `inlineHelpText`) owe a zh-CN help, so `pnpm check:i18n-coverage` went red (320 to 321 untranslated declared strings). Fixed by translating it in `examples/app-showcase/src/system/translations/index.ts` (one zh-CN `help` string), not by raising the baseline. Gate green again at 320. ## Tests New `packages/cli/test/i18n-extract-inline-help-text.test.ts` (6 cases): inlineHelpText emitted as the inline text; source order help, inlineHelpText, description; inlineHelpText-only field plus an `en` bundle with `help` leaves the default locale unreported; a non-default locale missing `help` is still reported; description-only case unchanged; no-inline-source case unchanged. - Fix commit fc0e06c: 6/6 pass. - Ablation on the committed fix (anchor replaced back to `field.help ?? field.description` through `scripts/ablation-replace.mjs`, anchor 1 to 0, blob changed): 3 of 6 fail (the inline emission, source-order and default-locale cases), then restored, blob equal to HEAD and `git diff HEAD` empty. The tests import `../src`, so no rebuild leg applies. - `pnpm --filter @objectstack/cli exec vitest run --project unit test/i18n`: 31 files, 341 tests pass. `@objectstack/example-showcase` test: 39 files, 484 tests pass. `@objectstack/cli typecheck`: exit 0. ## Gates (local subset, final head 8c1cdf8) All 72 derived families run and reconciled with `dispatch-gates --ran` (72 derived, 72 run, 0 NOT-MEASURED). Every one exits 0 on the final tree after prerequisite builds, except `check:dual-build-cjs-loads`: exit 3, PREREQUISITE NOT MET (needs a full `pnpm build`), NOT MEASURED locally; CI runs it. Scoped eslint over the touched files: exit 0. ## Acceptance hotsrm's case (inlineHelpText plus an `en` help, no zh-CN entry) passes the strict coverage check: pinned by the third test case. hotsrm can drop its temporary zh-CN bundle. --- _Generated by [Claude Code](https://claude.ai/code/session_01R1q2xdtYgU6FKrZ7LgjPCC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 769ac80 commit 1252db9

4 files changed

Lines changed: 85 additions & 2 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os i18n check` reads a field's `inlineHelpText` as the default-locale text of its `help` key
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The field help key (`objects.OBJECT.fields.FIELD.help`) took its inline source from `help` then `description` only, while `FieldSchema` spells help text `inlineHelpText`. An app that authored `inlineHelpText` and translated `help` into one locale had its default locale reported as missing a translation. The inline source is now `help`, then `inlineHelpText`, then `description`, like an authored label; a non-default locale with no `help` is still reported as a gap.

‎examples/app-showcase/src/system/translations/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ export const ShowcaseTranslationBundle = {
630630
label: '阶段',
631631
options: { new: '新建', working: '跟进中', qualified: '合格', closed: '已关闭' },
632632
},
633-
lead_score: { label: '线索评分' },
633+
lead_score: { label: '线索评分', help: '由评分规则计算,不可手动编辑。' },
634634
notes: { label: '备注' },
635635
},
636636
// #5420 — `ContactViews` had never been listed in the config's `views:`

‎packages/cli/src/utils/i18n-extract.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1377,7 +1377,16 @@ export function collectExpectedEntries(
13771377
{ objectName },
13781378
);
13791379

1380-
pushOptional(out, ['objects', objectName, 'fields', fieldName, 'help'], field.help ?? field.description, 'field', { objectName });
1380+
// `inlineHelpText` is the key `FieldSchema` spells help text with, so it
1381+
// is the authored default-locale text of `help` — after `help`, before
1382+
// `description` — exactly as the authored label is for `label` (#15711).
1383+
pushOptional(
1384+
out,
1385+
['objects', objectName, 'fields', fieldName, 'help'],
1386+
field.help ?? field.inlineHelpText ?? field.description,
1387+
'field',
1388+
{ objectName },
1389+
);
13811390
pushOptional(out, ['objects', objectName, 'fields', fieldName, 'placeholder'], field.placeholder, 'field', { objectName });
13821391

13831392
// Options — accept either `{value, label}[]` arrays or a record map.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license.
2+
//
3+
// A field spells its help text `inlineHelpText` (`FieldSchema`), so that is the
4+
// authored default-locale text of `objects.<o>.fields.<f>.help` — the same
5+
// rule an authored label follows. Before, the key's inline source read only
6+
// `help ?? description`: an app authoring `inlineHelpText` and translating
7+
// `help` into one locale had the DEFAULT locale reported missing.
8+
9+
import { describe, it, expect } from 'vitest';
10+
import { collectExpectedEntries } from '../src/utils/i18n-extract';
11+
import { computeI18nCoverage } from '../src/utils/i18n-coverage';
12+
13+
const HELP_KEY = 'objects.kpi.fields.score.help';
14+
15+
const stack = (field: Record<string, unknown>, translations: any[]): any => ({
16+
i18n: { defaultLocale: 'zh-CN', supportedLocales: ['zh-CN', 'en'] },
17+
objects: [{ name: 'kpi', label: '指标', fields: { score: { label: '分数', ...field } } }],
18+
translations,
19+
});
20+
21+
const helpIssues = (report: ReturnType<typeof computeI18nCoverage>, locale: string) =>
22+
report.issues.filter((i) => i.key === HELP_KEY && i.locale === locale);
23+
24+
const enHelp = { en: { objects: { kpi: { fields: { score: { help: 'Full score' } } } } } };
25+
26+
describe('the field help key takes `inlineHelpText` as its default-locale source', () => {
27+
it('emits the key inline, from inlineHelpText', () => {
28+
const entry = collectExpectedEntries(stack({ inlineHelpText: '满分' }, [])).find((e) => e.path.join('.') === HELP_KEY);
29+
expect(entry?.inline).toBe('满分');
30+
});
31+
32+
it('orders the sources help, then inlineHelpText, then description', () => {
33+
const inlineOf = (field: Record<string, unknown>) =>
34+
collectExpectedEntries(stack(field, [])).find((e) => e.path.join('.') === HELP_KEY)?.inline;
35+
expect(inlineOf({ help: 'H', inlineHelpText: 'I', description: 'D' })).toBe('H');
36+
expect(inlineOf({ inlineHelpText: 'I', description: 'D' })).toBe('I');
37+
expect(inlineOf({ description: 'D' })).toBe('D');
38+
});
39+
40+
it('does not report the default locale when an en bundle carries help and no zh-CN entry exists', () => {
41+
const report = computeI18nCoverage(stack({ inlineHelpText: '满分' }, [enHelp]), { strict: true });
42+
expect(helpIssues(report, 'zh-CN')).toEqual([]);
43+
});
44+
45+
it('still reports a non-default locale that carries no help', () => {
46+
const report = computeI18nCoverage(stack({ inlineHelpText: '满分' }, [{ 'zh-CN': {} }, enHelp]), {
47+
strict: true,
48+
defaultLocale: 'en',
49+
});
50+
// `en` is the default here: zh-CN is the non-default locale and owes help.
51+
expect(helpIssues(report, 'zh-CN').length).toBe(1);
52+
expect(helpIssues(report, 'en')).toEqual([]);
53+
});
54+
55+
it('keeps the description-only case unchanged: description still seeds the default locale', () => {
56+
const report = computeI18nCoverage(stack({ description: '说明' }, [enHelp]), { strict: true });
57+
expect(helpIssues(report, 'zh-CN')).toEqual([]);
58+
});
59+
60+
it('keeps the no-inline-source case unchanged: the default locale is still reported', () => {
61+
const report = computeI18nCoverage(stack({}, [enHelp]), { strict: true });
62+
expect(helpIssues(report, 'zh-CN').length).toBe(1);
63+
});
64+
});

0 commit comments

Comments
 (0)