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
10 changes: 10 additions & 0 deletions .changeset/25683-inline-help-default-locale.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/cli': patch
---

fix(cli): `os i18n check` reads a field's `inlineHelpText` as the default-locale text of its `help` key

Clause-②: no
Metadata-protocol: none

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.
2 changes: 1 addition & 1 deletion examples/app-showcase/src/system/translations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ export const ShowcaseTranslationBundle = {
label: '阶段',
options: { new: '新建', working: '跟进中', qualified: '合格', closed: '已关闭' },
},
lead_score: { label: '线索评分' },
lead_score: { label: '线索评分', help: '由评分规则计算,不可手动编辑。' },
notes: { label: '备注' },
},
// #5420 — `ContactViews` had never been listed in the config's `views:`
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/utils/i18n-extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1377,7 +1377,16 @@ export function collectExpectedEntries(
{ objectName },
);

pushOptional(out, ['objects', objectName, 'fields', fieldName, 'help'], field.help ?? field.description, 'field', { objectName });
// `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).
pushOptional(
out,
['objects', objectName, 'fields', fieldName, 'help'],
field.help ?? field.inlineHelpText ?? field.description,
'field',
{ objectName },
);
pushOptional(out, ['objects', objectName, 'fields', fieldName, 'placeholder'], field.placeholder, 'field', { objectName });

// Options — accept either `{value, label}[]` arrays or a record map.
Expand Down
64 changes: 64 additions & 0 deletions packages/cli/test/i18n-extract-inline-help-text.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license.
//
// A field spells its help text `inlineHelpText` (`FieldSchema`), so that is the
// authored default-locale text of `objects.<o>.fields.<f>.help` — the same
// rule an authored label follows. Before, the key's inline source read only
// `help ?? description`: an app authoring `inlineHelpText` and translating
// `help` into one locale had the DEFAULT locale reported missing.

import { describe, it, expect } from 'vitest';
import { collectExpectedEntries } from '../src/utils/i18n-extract';
import { computeI18nCoverage } from '../src/utils/i18n-coverage';

const HELP_KEY = 'objects.kpi.fields.score.help';

const stack = (field: Record<string, unknown>, translations: any[]): any => ({
i18n: { defaultLocale: 'zh-CN', supportedLocales: ['zh-CN', 'en'] },
objects: [{ name: 'kpi', label: '指标', fields: { score: { label: '分数', ...field } } }],
translations,
});

const helpIssues = (report: ReturnType<typeof computeI18nCoverage>, locale: string) =>
report.issues.filter((i) => i.key === HELP_KEY && i.locale === locale);

const enHelp = { en: { objects: { kpi: { fields: { score: { help: 'Full score' } } } } } };

describe('the field help key takes `inlineHelpText` as its default-locale source', () => {
it('emits the key inline, from inlineHelpText', () => {
const entry = collectExpectedEntries(stack({ inlineHelpText: '满分' }, [])).find((e) => e.path.join('.') === HELP_KEY);
expect(entry?.inline).toBe('满分');
});

it('orders the sources help, then inlineHelpText, then description', () => {
const inlineOf = (field: Record<string, unknown>) =>
collectExpectedEntries(stack(field, [])).find((e) => e.path.join('.') === HELP_KEY)?.inline;
expect(inlineOf({ help: 'H', inlineHelpText: 'I', description: 'D' })).toBe('H');
expect(inlineOf({ inlineHelpText: 'I', description: 'D' })).toBe('I');
expect(inlineOf({ description: 'D' })).toBe('D');
});

it('does not report the default locale when an en bundle carries help and no zh-CN entry exists', () => {
const report = computeI18nCoverage(stack({ inlineHelpText: '满分' }, [enHelp]), { strict: true });
expect(helpIssues(report, 'zh-CN')).toEqual([]);
});

it('still reports a non-default locale that carries no help', () => {
const report = computeI18nCoverage(stack({ inlineHelpText: '满分' }, [{ 'zh-CN': {} }, enHelp]), {
strict: true,
defaultLocale: 'en',
});
// `en` is the default here: zh-CN is the non-default locale and owes help.
expect(helpIssues(report, 'zh-CN').length).toBe(1);
expect(helpIssues(report, 'en')).toEqual([]);
});

it('keeps the description-only case unchanged: description still seeds the default locale', () => {
const report = computeI18nCoverage(stack({ description: '说明' }, [enHelp]), { strict: true });
expect(helpIssues(report, 'zh-CN')).toEqual([]);
});

it('keeps the no-inline-source case unchanged: the default locale is still reported', () => {
const report = computeI18nCoverage(stack({}, [enHelp]), { strict: true });
expect(helpIssues(report, 'zh-CN').length).toBe(1);
});
});
Loading