diff --git a/.changeset/25683-inline-help-default-locale.md b/.changeset/25683-inline-help-default-locale.md new file mode 100644 index 00000000000..3851ca432f8 --- /dev/null +++ b/.changeset/25683-inline-help-default-locale.md @@ -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. diff --git a/examples/app-showcase/src/system/translations/index.ts b/examples/app-showcase/src/system/translations/index.ts index b578aab68f8..3b152cb4909 100644 --- a/examples/app-showcase/src/system/translations/index.ts +++ b/examples/app-showcase/src/system/translations/index.ts @@ -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:` diff --git a/packages/cli/src/utils/i18n-extract.ts b/packages/cli/src/utils/i18n-extract.ts index e930860d287..73ad3d73579 100644 --- a/packages/cli/src/utils/i18n-extract.ts +++ b/packages/cli/src/utils/i18n-extract.ts @@ -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. diff --git a/packages/cli/test/i18n-extract-inline-help-text.test.ts b/packages/cli/test/i18n-extract-inline-help-text.test.ts new file mode 100644 index 00000000000..8d9989f6c3c --- /dev/null +++ b/packages/cli/test/i18n-extract-inline-help-text.test.ts @@ -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..fields..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, translations: any[]): any => ({ + i18n: { defaultLocale: 'zh-CN', supportedLocales: ['zh-CN', 'en'] }, + objects: [{ name: 'kpi', label: '指标', fields: { score: { label: '分数', ...field } } }], + translations, +}); + +const helpIssues = (report: ReturnType, 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) => + 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); + }); +});