@@ -1063,6 +1063,69 @@ describe('RestServer', () => {
10631063 // First match wins → the more-specific export route must come first.
10641064 expect ( exportIdx ) . toBeLessThan ( getByIdIdx ) ;
10651065 } ) ;
1066+
1067+ // Regression: the header row must carry the same localized field labels the
1068+ // UI renders (Accept-Language / ?locale=), not the raw `field.label` from the
1069+ // object schema. The route translates the schema via `translateMetaItem`
1070+ // before building the header, so a locale switch flips the header language.
1071+ it ( 'localizes the export header row to the request locale' , async ( ) => {
1072+ // Schema field labels are deliberately distinct from the bundle so a
1073+ // green assertion proves the bundle was applied (not the raw labels).
1074+ const NAMED_SCHEMA = {
1075+ name : 'task' ,
1076+ fields : [
1077+ { name : 'id' , type : 'text' , label : 'ID' } ,
1078+ { name : 'title' , type : 'text' , label : 'RawTitle' } ,
1079+ { name : 'done' , type : 'boolean' , label : 'RawDone' } ,
1080+ ] ,
1081+ } ;
1082+ const bundle : Record < string , any > = {
1083+ en : { objects : { task : { fields : { title : { label : 'Title' } , done : { label : 'Done' } } } } } ,
1084+ zh : { objects : { task : { fields : { title : { label : '标题' } , done : { label : '完成' } } } } } ,
1085+ } ;
1086+ const i18n = {
1087+ getLocales : ( ) => [ 'en' , 'zh' ] ,
1088+ getTranslations : ( l : string ) => bundle [ l ] ,
1089+ getDefaultLocale : ( ) => 'en' ,
1090+ } ;
1091+
1092+ const p : any = createMockProtocol ( ) ;
1093+ p . getMetaItem = vi . fn ( ) . mockResolvedValue ( { type : 'object' , name : 'task' , item : NAMED_SCHEMA } ) ;
1094+ // First page returns one row, subsequent pages are empty (ends the stream).
1095+ p . findData = vi . fn ( async ( { query } : any ) =>
1096+ ( query ?. $skip ?? 0 ) === 0 ? { data : [ { id : '1' , title : 'x' , done : true } ] } : { data : [ ] } ,
1097+ ) ;
1098+
1099+ // i18nServiceProvider is the 14th constructor arg (after server, protocol,
1100+ // config, and the 10 service providers preceding it).
1101+ const rest = new RestServer (
1102+ server as any , p as any , { } ,
1103+ undefined , undefined , undefined , undefined , undefined ,
1104+ undefined , undefined , undefined , undefined , undefined ,
1105+ async ( ) => i18n as any ,
1106+ ) ;
1107+ rest . registerRoutes ( ) ;
1108+ const route = getExportRoute ( rest ) ;
1109+
1110+ // locale=zh → Chinese header; `id` has no override so it keeps its label.
1111+ const zh = makeRes ( ) ;
1112+ await route ! . handler ( {
1113+ params : { object : 'task' } ,
1114+ query : { format : 'csv' , fields : 'id,title,done' , locale : 'zh' } ,
1115+ headers : { } ,
1116+ } as any , zh . res ) ;
1117+ expect ( zh . chunks . join ( '' ) . split ( '\r\n' ) [ 0 ] ) . toBe ( 'ID,标题,完成' ) ;
1118+
1119+ // Default locale (en, no ?locale=) → English header from the bundle,
1120+ // i.e. 'Title'/'Done', never the raw 'RawTitle'/'RawDone'.
1121+ const en = makeRes ( ) ;
1122+ await route ! . handler ( {
1123+ params : { object : 'task' } ,
1124+ query : { format : 'csv' , fields : 'id,title,done' } ,
1125+ headers : { } ,
1126+ } as any , en . res ) ;
1127+ expect ( en . chunks . join ( '' ) . split ( '\r\n' ) [ 0 ] ) . toBe ( 'ID,Title,Done' ) ;
1128+ } ) ;
10661129 } ) ;
10671130
10681131 // -----------------------------------------------------------------------
0 commit comments