Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/29221-i18n-extract-field-help-empty-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@objectstack/cli': patch
---

fix(cli): `os i18n extract` scaffolds a field's description when its `inlineHelpText` is empty

Clause-②: no
Metadata-protocol: none

Nothing changes in what a metadata author can write. What changes is the seed `os i18n extract` (and the `os i18n check` coverage gate) takes for a field's `objects.OBJECT.fields.FIELD.help` entry.

- **Before.** The seed was `help ?? inlineHelpText ?? description`, which stops at an empty string. A field with `inlineHelpText: ''` (or `help: ''`) and a `description` got a help entry with no seed, so the description, which is the line the console draws for that field, was never offered for translation.
- **Now.** The first link that carries authored text wins: an empty string reads as absent, as it does in the console's help line (`inlineHelpText || description`). Whitespace-only text still counts as authored, as it does in the console.
- **Unchanged.** `help` still wins over `inlineHelpText`, and a non-empty `inlineHelpText` still wins over `description`. A field whose links are all empty still gets the seed-less entry it got before.
12 changes: 11 additions & 1 deletion packages/cli/src/utils/i18n-extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1483,10 +1483,20 @@ export function collectExpectedEntries(
// `inlineHelpText` is the key `FieldSchema` spells help text with, so it
// is the authored default-locale text of `help` — after `help`, before
// `description` — exactly as the authored label is for `label` (#15711).
//
// The FIRST AUTHORED link wins, not the first defined one: the console
// draws `inlineHelpText || description` (`fieldHelpText` in
// `@object-ui/fields`), so an empty `inlineHelpText` reads as absent
// there and the description is the line people see. A `??` chain
// stopped at that `''` and scaffolded no seed, leaving the drawn
// description out of the bundle (#29221). `asAuthoredLabel` is the one
// "authored" test this file uses (a non-empty string or a non-empty
// inline locale map); whitespace-only text stays authored, as it does
// for the console's `||` and the resolver's non-empty check.
pushOptional(
out,
['objects', objectName, 'fields', fieldName, 'help'],
field.help ?? field.inlineHelpText ?? field.description,
asAuthoredLabel(field.help) ?? asAuthoredLabel(field.inlineHelpText) ?? asAuthoredLabel(field.description),
'field',
{ objectName },
);
Expand Down
69 changes: 69 additions & 0 deletions packages/cli/test/i18n-extract-inline-help-text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,72 @@ describe('the field help key takes `inlineHelpText` as its default-locale source
expect(helpIssues(report, 'zh-CN').length).toBe(1);
});
});

// The extractor seeds the key from the FIRST AUTHORED link of
// `help`, `inlineHelpText`, `description`. The console draws a field's help
// line from `inlineHelpText || description` (`fieldHelpText` in
// `@object-ui/fields`), so an empty string reads as absent there; a `??` chain
// stopped at `inlineHelpText: ''` and scaffolded no seed, leaving the drawn
// description out of the bundle (#29221).
describe('the field help key skips an empty link, as the console does', () => {
const entryOf = (field: Record<string, unknown>) =>
collectExpectedEntries(stack(field, [])).find((e) => e.path.join('.') === HELP_KEY);

/** The console's reader (`fieldHelpText` in `@object-ui/fields`), restated. */
const consoleHelpLine = (field: Record<string, unknown>) =>
(field.inlineHelpText as string | undefined) || (field.description as string | undefined) || undefined;

it('`inlineHelpText: \'\'` beside a description scaffolds the description', () => {
const entry = entryOf({ inlineHelpText: '', description: 'B' });
expect(entry?.sourceValue).toBe('B');
expect(entry?.inline).toBe('B');
});

it('`help: \'\'` beside a description scaffolds the description', () => {
const entry = entryOf({ help: '', description: 'B' });
expect(entry?.sourceValue).toBe('B');
expect(entry?.inline).toBe('B');
});

it('`help: \'\'` beside an `inlineHelpText` scaffolds the `inlineHelpText`', () => {
expect(entryOf({ help: '', inlineHelpText: 'I', description: 'B' })?.sourceValue).toBe('I');
});

it('a non-empty `inlineHelpText` still wins over the description', () => {
expect(entryOf({ inlineHelpText: 'I', description: 'B' })?.sourceValue).toBe('I');
});

it('`help` still wins over both', () => {
expect(entryOf({ help: 'H', inlineHelpText: 'I', description: 'B' })?.sourceValue).toBe('H');
expect(entryOf({ help: 'H', inlineHelpText: '', description: 'B' })?.sourceValue).toBe('H');
});

it('whitespace-only `inlineHelpText` stays the source: the console\'s `||` draws it too', () => {
const field = { inlineHelpText: ' ', description: 'B' };
expect(entryOf(field)?.sourceValue).toBe(' ');
expect(consoleHelpLine(field)).toBe(' ');
});

it('every link empty still emits the seed-less entry, as before', () => {
const entry = entryOf({ help: '', inlineHelpText: '', description: '' });
expect(entry).toBeDefined();
expect(entry?.sourceValue).toBeUndefined();
expect(entry?.inline).toBeUndefined();
});

it('agrees with the line the console draws, for every field that authors no `help`', () => {
const shapes: Record<string, unknown>[] = [
{ description: 'B' },
{ inlineHelpText: 'I' },
{ inlineHelpText: 'I', description: 'B' },
{ inlineHelpText: '', description: 'B' },
{ inlineHelpText: 'I', description: '' },
];
for (const field of shapes) expect(entryOf(field)?.sourceValue).toBe(consoleHelpLine(field));
});

it('the coverage gate counts the default locale as covered by the scaffolded description', () => {
const report = computeI18nCoverage(stack({ inlineHelpText: '', description: '说明' }, [enHelp]), { strict: true });
expect(helpIssues(report, 'zh-CN')).toEqual([]);
});
});
Loading