@@ -3548,6 +3548,175 @@ describe('translateObject — catalog vs explicit override (#8284)', () => {
35483548 } ) ;
35493549} ) ;
35503550
3551+ // ────────────────────────────────────────────────────────────────────────────
3552+ // translateObject — a field's translated help is served on `description` (#21948)
3553+ // ────────────────────────────────────────────────────────────────────────────
3554+
3555+ import { FieldSchema } from '../data/field.zod' ;
3556+
3557+ /**
3558+ * #21948 — the bundle's `objects.<object>.fields.<field>.help` entry is the
3559+ * translation of the field's `description` (the i18n extractor writes it from
3560+ * that key), and it used to be served on a `help` key `FieldSchema` does not
3561+ * declare. Every consumer that reads only declared keys rendered the English
3562+ * `description`, and the console warned once per such field on every load.
3563+ *
3564+ * The entry is served on `description` now, under ADR-0029 D9.2a: the catalog
3565+ * applies only while the served `description` still equals the packaged
3566+ * field's (`valueOverridesPackagedBase`), and with no packaged base it applies.
3567+ */
3568+ describe ( 'translateObject — a field\'s translated help is served on `description`' , ( ) => {
3569+ const TWO_FACTOR_EN =
3570+ 'Whether two-factor authentication is enabled for this user. Maintained by the better-auth `twoFactor` plugin.' ;
3571+ const TWO_FACTOR_ZH = '该用户是否已启用双因素认证。由 better-auth 的 `twoFactor` 插件维护。' ;
3572+
3573+ /** The packaged declaration, as the owner contributor holds it. */
3574+ const PACKAGED = {
3575+ name : 'sys_user' ,
3576+ label : 'User' ,
3577+ fields : {
3578+ two_factor_enabled : {
3579+ name : 'two_factor_enabled' ,
3580+ type : 'boolean' ,
3581+ label : 'Two-Factor Enabled' ,
3582+ description : TWO_FACTOR_EN ,
3583+ } ,
3584+ phone : {
3585+ name : 'phone' ,
3586+ type : 'text' ,
3587+ label : 'Phone' ,
3588+ description : 'Contact number.' ,
3589+ inlineHelpText : 'Include the country code.' ,
3590+ } ,
3591+ nickname : { name : 'nickname' , type : 'text' , label : 'Nickname' } ,
3592+ } ,
3593+ } ;
3594+
3595+ /**
3596+ * What the extractor writes: `en` repeats the packaged `description` under
3597+ * `help`, `zh-CN` is its translation. `nickname` has no entry at all.
3598+ */
3599+ const BUNDLE : TranslationBundle = {
3600+ en : {
3601+ objects : {
3602+ sys_user : {
3603+ fields : {
3604+ two_factor_enabled : { label : 'Two-Factor Enabled' , help : TWO_FACTOR_EN } ,
3605+ phone : { label : 'Phone' , help : 'Contact number.' } ,
3606+ } ,
3607+ } ,
3608+ } ,
3609+ } as any ,
3610+ 'zh-CN' : {
3611+ objects : {
3612+ sys_user : {
3613+ fields : {
3614+ two_factor_enabled : { label : '已启用双因素认证' , help : TWO_FACTOR_ZH } ,
3615+ phone : { label : '电话' , help : '联系电话。' } ,
3616+ } ,
3617+ } ,
3618+ } ,
3619+ } as any ,
3620+ } ;
3621+
3622+ const clone = < V > ( value : V ) : V => JSON . parse ( JSON . stringify ( value ) ) ;
3623+ const fieldsOf = ( out : unknown ) => ( out as { fields : Record < string , Record < string , unknown > > } ) . fields ;
3624+ const unrecognizedKeys = ( def : unknown ) => {
3625+ const parsed = FieldSchema . safeParse ( def ) ;
3626+ return parsed . success ? [ ] : parsed . error . issues . filter ( ( issue ) => issue . code === 'unrecognized_keys' ) ;
3627+ } ;
3628+
3629+ it ( 'serves the zh-CN translation on `description`, carries no `help` key, and parses with no unrecognized key' , ( ) => {
3630+ const field = fieldsOf ( translateObject ( clone ( PACKAGED ) , BUNDLE , { locale : 'zh-CN' , packagedBase : PACKAGED } ) )
3631+ . two_factor_enabled ;
3632+ expect ( field . description ) . toBe ( TWO_FACTOR_ZH ) ;
3633+ expect ( 'help' in field ) . toBe ( false ) ;
3634+ expect ( field . label ) . toBe ( '已启用双因素认证' ) ;
3635+ expect ( unrecognizedKeys ( field ) ) . toEqual ( [ ] ) ;
3636+ } ) ;
3637+
3638+ it ( 'leaves a field with no bundle entry exactly as it came — the control' , ( ) => {
3639+ const doc = clone ( PACKAGED ) ;
3640+ const field = fieldsOf ( translateObject ( doc , BUNDLE , { locale : 'zh-CN' , packagedBase : PACKAGED } ) ) . nickname ;
3641+ expect ( field ) . toEqual ( PACKAGED . fields . nickname ) ;
3642+ expect ( 'description' in field ) . toBe ( false ) ;
3643+ expect ( 'help' in field ) . toBe ( false ) ;
3644+ } ) ;
3645+
3646+ it ( 'a field declaring both `description` and `inlineHelpText` gets the translation on `description`; `inlineHelpText` is untouched' , ( ) => {
3647+ const field = fieldsOf ( translateObject ( clone ( PACKAGED ) , BUNDLE , { locale : 'zh-CN' , packagedBase : PACKAGED } ) ) . phone ;
3648+ expect ( field . description ) . toBe ( '联系电话。' ) ;
3649+ expect ( field . inlineHelpText ) . toBe ( 'Include the country code.' ) ;
3650+ expect ( 'help' in field ) . toBe ( false ) ;
3651+ expect ( unrecognizedKeys ( field ) ) . toEqual ( [ ] ) ;
3652+ } ) ;
3653+
3654+ it ( 'keeps a `description` that diverged from the packaged field, in every locale, through the type dispatch' , ( ) => {
3655+ // The REST boundary reaches `translateObject` through the dispatcher, so the
3656+ // packaged base has to arrive there for the comparison to be made at all.
3657+ const served = clone ( PACKAGED ) ;
3658+ served . fields . two_factor_enabled . description = 'Edited by the tenant.' ;
3659+ for ( const locale of [ 'zh-CN' , 'en' ] ) {
3660+ const fields = fieldsOf ( translateMetadataDocument ( 'object' , clone ( served ) , BUNDLE , { locale, packagedBase : PACKAGED } ) ) ;
3661+ expect ( fields . two_factor_enabled . description ) . toBe ( 'Edited by the tenant.' ) ;
3662+ expect ( 'help' in fields . two_factor_enabled ) . toBe ( false ) ;
3663+ }
3664+ // Judged per field: the undiverged sibling is still translated…
3665+ const zh = fieldsOf ( translateMetadataDocument ( 'object' , clone ( served ) , BUNDLE , { locale : 'zh-CN' , packagedBase : PACKAGED } ) ) ;
3666+ expect ( zh . phone . description ) . toBe ( '联系电话。' ) ;
3667+ // …and the field `label` stays a flat `catalog ?? document`, as before.
3668+ expect ( zh . two_factor_enabled . label ) . toBe ( '已启用双因素认证' ) ;
3669+ } ) ;
3670+
3671+ it ( 'a field the packaged base does not declare counts as diverged' , ( ) => {
3672+ // An `objectExtensions` field is folded on after the owner's declaration,
3673+ // which is all the base carries.
3674+ const served = clone ( PACKAGED ) as any ;
3675+ served . fields . extra = { name : 'extra' , type : 'text' , label : 'Extra' , description : 'Added by an extension.' } ;
3676+ const bundle = clone ( BUNDLE ) as any ;
3677+ bundle [ 'zh-CN' ] . objects . sys_user . fields . extra = { help : '扩展添加。' } ;
3678+ const field = fieldsOf ( translateObject ( served , bundle , { locale : 'zh-CN' , packagedBase : PACKAGED } ) ) . extra ;
3679+ expect ( field . description ) . toBe ( 'Added by an extension.' ) ;
3680+ expect ( 'help' in field ) . toBe ( false ) ;
3681+ } ) ;
3682+
3683+ it ( 'with no packaged base, the catalog applies — "unknown" is not "authored"' , ( ) => {
3684+ const served = clone ( PACKAGED ) ;
3685+ served . fields . two_factor_enabled . description = 'Edited by the tenant.' ;
3686+ for ( const packagedBase of [ undefined , null ] ) {
3687+ const field = fieldsOf ( translateObject ( clone ( served ) , BUNDLE , { locale : 'zh-CN' , packagedBase } ) ) . two_factor_enabled ;
3688+ expect ( field . description ) . toBe ( TWO_FACTOR_ZH ) ;
3689+ expect ( 'help' in field ) . toBe ( false ) ;
3690+ }
3691+ expect ( fieldsOf ( translateObject ( clone ( served ) , BUNDLE , { locale : 'zh-CN' } ) ) . two_factor_enabled . description )
3692+ . toBe ( TWO_FACTOR_ZH ) ;
3693+ } ) ;
3694+
3695+ it ( 'an absent served `description` is not an override — the catalog fills it' , ( ) => {
3696+ const served = clone ( PACKAGED ) as any ;
3697+ delete served . fields . two_factor_enabled . description ;
3698+ const field = fieldsOf ( translateObject ( served , BUNDLE , { locale : 'zh-CN' , packagedBase : PACKAGED } ) ) . two_factor_enabled ;
3699+ expect ( field . description ) . toBe ( TWO_FACTOR_ZH ) ;
3700+ } ) ;
3701+
3702+ it ( 'judges an array-shaped field list against an array-shaped base by field name' , ( ) => {
3703+ const asArray = ( doc : typeof PACKAGED ) => ( { ...doc , fields : Object . values ( doc . fields ) } ) ;
3704+ const served = clone ( PACKAGED ) ;
3705+ served . fields . two_factor_enabled . description = 'Edited by the tenant.' ;
3706+ const out = translateObject ( asArray ( served ) as any , BUNDLE , { locale : 'zh-CN' , packagedBase : asArray ( PACKAGED ) } ) ;
3707+ const byName = Object . fromEntries ( ( out . fields as any [ ] ) . map ( ( f ) => [ f . name , f ] ) ) ;
3708+ expect ( byName . two_factor_enabled . description ) . toBe ( 'Edited by the tenant.' ) ;
3709+ expect ( byName . phone . description ) . toBe ( '联系电话。' ) ;
3710+ expect ( ( out . fields as any [ ] ) . some ( ( f ) => 'help' in f ) ) . toBe ( false ) ;
3711+ } ) ;
3712+
3713+ it ( 'does not mutate the input document' , ( ) => {
3714+ const doc = clone ( PACKAGED ) ;
3715+ translateObject ( doc , BUNDLE , { locale : 'zh-CN' , packagedBase : PACKAGED } ) ;
3716+ expect ( doc ) . toEqual ( PACKAGED ) ;
3717+ } ) ;
3718+ } ) ;
3719+
35513720// ════════════════════════════════════════════════════════════════════════════
35523721// Screen-flow copy resolvers (#7646 / #11287)
35533722// ════════════════════════════════════════════════════════════════════════════
0 commit comments