@@ -7,18 +7,21 @@ import {
77 ObjectTranslationDataSchema ,
88 TranslationFileOrganizationSchema ,
99 TranslationConfigSchema ,
10+ MessageFormatSchema ,
1011 ObjectTranslationNodeSchema ,
1112 AppTranslationBundleSchema ,
1213 TranslationDiffStatusSchema ,
1314 TranslationDiffItemSchema ,
1415 TranslationCoverageResultSchema ,
16+ CoverageBreakdownEntrySchema ,
1517 type TranslationBundle ,
1618 type ObjectTranslationData ,
1719 type TranslationConfig ,
1820 type ObjectTranslationNode ,
1921 type AppTranslationBundle ,
2022 type TranslationDiffItem ,
2123 type TranslationCoverageResult ,
24+ type CoverageBreakdownEntry ,
2225} from './translation.zod' ;
2326
2427describe ( 'LocaleSchema' , ( ) => {
@@ -474,6 +477,25 @@ describe('FieldTranslationSchema', () => {
474477 const result = FieldTranslationSchema . parse ( { } ) ;
475478 expect ( result ) . toBeDefined ( ) ;
476479 } ) ;
480+
481+ it ( 'should accept field with placeholder' , ( ) => {
482+ const result = FieldTranslationSchema . parse ( {
483+ label : 'Email' ,
484+ placeholder : 'Enter your email address' ,
485+ } ) ;
486+ expect ( result . placeholder ) . toBe ( 'Enter your email address' ) ;
487+ } ) ;
488+
489+ it ( 'should accept field with all properties including placeholder' , ( ) => {
490+ const result = FieldTranslationSchema . parse ( {
491+ label : '邮箱' ,
492+ help : '输入您的电子邮箱地址' ,
493+ placeholder : '例如:user@example.com' ,
494+ options : { work : '工作邮箱' , personal : '个人邮箱' } ,
495+ } ) ;
496+ expect ( result . label ) . toBe ( '邮箱' ) ;
497+ expect ( result . placeholder ) . toBe ( '例如:user@example.com' ) ;
498+ } ) ;
477499} ) ;
478500
479501// ============================================================================
@@ -622,6 +644,38 @@ describe('TranslationConfigSchema', () => {
622644 } ) ,
623645 ) . toThrow ( ) ;
624646 } ) ;
647+
648+ it ( 'should default messageFormat to simple' , ( ) => {
649+ const config = TranslationConfigSchema . parse ( {
650+ defaultLocale : 'en' ,
651+ supportedLocales : [ 'en' ] ,
652+ } ) ;
653+ expect ( config . messageFormat ) . toBe ( 'simple' ) ;
654+ } ) ;
655+
656+ it ( 'should accept ICU message format config' , ( ) => {
657+ const config = TranslationConfigSchema . parse ( {
658+ defaultLocale : 'en' ,
659+ supportedLocales : [ 'en' , 'ar-SA' ] ,
660+ messageFormat : 'icu' ,
661+ } ) ;
662+ expect ( config . messageFormat ) . toBe ( 'icu' ) ;
663+ } ) ;
664+ } ) ;
665+
666+ // ============================================================================
667+ // MessageFormatSchema
668+ // ============================================================================
669+
670+ describe ( 'MessageFormatSchema' , ( ) => {
671+ it ( 'should accept icu and simple' , ( ) => {
672+ expect ( MessageFormatSchema . parse ( 'icu' ) ) . toBe ( 'icu' ) ;
673+ expect ( MessageFormatSchema . parse ( 'simple' ) ) . toBe ( 'simple' ) ;
674+ } ) ;
675+
676+ it ( 'should reject invalid format' , ( ) => {
677+ expect ( ( ) => MessageFormatSchema . parse ( 'mf2' ) ) . toThrow ( ) ;
678+ } ) ;
625679} ) ;
626680
627681// ============================================================================
@@ -703,6 +757,25 @@ describe('ObjectTranslationNodeSchema', () => {
703757 expect ( node . fields ?. stage . label ) . toBe ( 'Stage' ) ;
704758 expect ( node . _views ?. pipeline . label ) . toBe ( 'Pipeline View' ) ;
705759 } ) ;
760+
761+ it ( 'should accept node with _notifications and _errors' , ( ) => {
762+ const node : ObjectTranslationNode = ObjectTranslationNodeSchema . parse ( {
763+ label : 'Order' ,
764+ _notifications : {
765+ order_shipped : { title : 'Order Shipped' , body : 'Your order has been shipped.' } ,
766+ order_cancelled : { title : 'Order Cancelled' } ,
767+ } ,
768+ _errors : {
769+ insufficient_stock : 'Not enough stock for this order.' ,
770+ payment_failed : 'Payment could not be processed.' ,
771+ } ,
772+ } ) ;
773+ expect ( node . _notifications ?. order_shipped . title ) . toBe ( 'Order Shipped' ) ;
774+ expect ( node . _notifications ?. order_shipped . body ) . toBe ( 'Your order has been shipped.' ) ;
775+ expect ( node . _notifications ?. order_cancelled . title ) . toBe ( 'Order Cancelled' ) ;
776+ expect ( node . _errors ?. insufficient_stock ) . toBe ( 'Not enough stock for this order.' ) ;
777+ expect ( node . _errors ?. payment_failed ) . toBe ( 'Payment could not be processed.' ) ;
778+ } ) ;
706779} ) ;
707780
708781// ============================================================================
@@ -822,6 +895,45 @@ describe('AppTranslationBundleSchema', () => {
822895 expect ( zh . nav ?. home ) . toBe ( '首页' ) ;
823896 expect ( zh . messages ?. [ 'common.save' ] ) . toBe ( '保存' ) ;
824897 } ) ;
898+
899+ it ( 'should accept bundle with _meta for RTL locale' , ( ) => {
900+ const bundle : AppTranslationBundle = AppTranslationBundleSchema . parse ( {
901+ _meta : { locale : 'ar-SA' , direction : 'rtl' } ,
902+ messages : { 'common.save' : 'حفظ' } ,
903+ } ) ;
904+ expect ( bundle . _meta ?. locale ) . toBe ( 'ar-SA' ) ;
905+ expect ( bundle . _meta ?. direction ) . toBe ( 'rtl' ) ;
906+ } ) ;
907+
908+ it ( 'should accept bundle with namespace for plugin isolation' , ( ) => {
909+ const bundle : AppTranslationBundle = AppTranslationBundleSchema . parse ( {
910+ namespace : 'plugin-helpdesk' ,
911+ o : { ticket : { label : 'Ticket' } } ,
912+ } ) ;
913+ expect ( bundle . namespace ) . toBe ( 'plugin-helpdesk' ) ;
914+ } ) ;
915+
916+ it ( 'should accept bundle with global notifications and errors' , ( ) => {
917+ const bundle : AppTranslationBundle = AppTranslationBundleSchema . parse ( {
918+ notifications : {
919+ system_update : { title : 'System Update' , body : 'A new version is available.' } ,
920+ } ,
921+ errors : {
922+ unauthorized : 'You are not authorized to perform this action.' ,
923+ not_found : 'The requested resource was not found.' ,
924+ } ,
925+ } ) ;
926+ expect ( bundle . notifications ?. system_update . title ) . toBe ( 'System Update' ) ;
927+ expect ( bundle . errors ?. unauthorized ) . toBe ( 'You are not authorized to perform this action.' ) ;
928+ } ) ;
929+
930+ it ( 'should accept bundle with _meta direction ltr' , ( ) => {
931+ const bundle = AppTranslationBundleSchema . parse ( {
932+ _meta : { direction : 'ltr' } ,
933+ } ) ;
934+ expect ( bundle . _meta ?. direction ) . toBe ( 'ltr' ) ;
935+ expect ( bundle . _meta ?. locale ) . toBeUndefined ( ) ;
936+ } ) ;
825937} ) ;
826938
827939// ============================================================================
@@ -883,6 +995,50 @@ describe('TranslationDiffItemSchema', () => {
883995 TranslationDiffItemSchema . parse ( { status : 'missing' , locale : 'en' } ) ,
884996 ) . toThrow ( ) ;
885997 } ) ;
998+
999+ it ( 'should accept diff item with sourceHash' , ( ) => {
1000+ const item = TranslationDiffItemSchema . parse ( {
1001+ key : 'o.account.label' ,
1002+ status : 'stale' ,
1003+ locale : 'zh-CN' ,
1004+ sourceHash : 'sha256:abc123' ,
1005+ } ) ;
1006+ expect ( item . sourceHash ) . toBe ( 'sha256:abc123' ) ;
1007+ } ) ;
1008+
1009+ it ( 'should accept diff item with AI suggestion fields' , ( ) => {
1010+ const item : TranslationDiffItem = TranslationDiffItemSchema . parse ( {
1011+ key : 'o.account.fields.website.label' ,
1012+ status : 'missing' ,
1013+ locale : 'zh-CN' ,
1014+ aiSuggested : '网站' ,
1015+ aiConfidence : 0.92 ,
1016+ } ) ;
1017+ expect ( item . aiSuggested ) . toBe ( '网站' ) ;
1018+ expect ( item . aiConfidence ) . toBe ( 0.92 ) ;
1019+ } ) ;
1020+
1021+ it ( 'should reject AI confidence above 1' , ( ) => {
1022+ expect ( ( ) =>
1023+ TranslationDiffItemSchema . parse ( {
1024+ key : 'o.account.label' ,
1025+ status : 'missing' ,
1026+ locale : 'en' ,
1027+ aiConfidence : 1.5 ,
1028+ } ) ,
1029+ ) . toThrow ( ) ;
1030+ } ) ;
1031+
1032+ it ( 'should reject AI confidence below 0' , ( ) => {
1033+ expect ( ( ) =>
1034+ TranslationDiffItemSchema . parse ( {
1035+ key : 'o.account.label' ,
1036+ status : 'missing' ,
1037+ locale : 'en' ,
1038+ aiConfidence : - 0.1 ,
1039+ } ) ,
1040+ ) . toThrow ( ) ;
1041+ } ) ;
8861042} ) ;
8871043
8881044// ============================================================================
@@ -963,4 +1119,82 @@ describe('TranslationCoverageResultSchema', () => {
9631119 } ) ,
9641120 ) . toThrow ( ) ;
9651121 } ) ;
1122+
1123+ it ( 'should accept result with breakdown' , ( ) => {
1124+ const result : TranslationCoverageResult = TranslationCoverageResultSchema . parse ( {
1125+ locale : 'zh-CN' ,
1126+ totalKeys : 100 ,
1127+ translatedKeys : 80 ,
1128+ missingKeys : 20 ,
1129+ redundantKeys : 0 ,
1130+ staleKeys : 0 ,
1131+ coveragePercent : 80 ,
1132+ items : [ ] ,
1133+ breakdown : [
1134+ { group : 'fields' , totalKeys : 60 , translatedKeys : 50 , coveragePercent : 83.3 } ,
1135+ { group : 'views' , totalKeys : 20 , translatedKeys : 15 , coveragePercent : 75 } ,
1136+ { group : 'actions' , totalKeys : 10 , translatedKeys : 10 , coveragePercent : 100 } ,
1137+ { group : 'messages' , totalKeys : 10 , translatedKeys : 5 , coveragePercent : 50 } ,
1138+ ] ,
1139+ } ) ;
1140+ expect ( result . breakdown ) . toHaveLength ( 4 ) ;
1141+ expect ( result . breakdown ! [ 0 ] . group ) . toBe ( 'fields' ) ;
1142+ expect ( result . breakdown ! [ 0 ] . coveragePercent ) . toBe ( 83.3 ) ;
1143+ expect ( result . breakdown ! [ 2 ] . coveragePercent ) . toBe ( 100 ) ;
1144+ } ) ;
1145+
1146+ it ( 'should accept result without breakdown (optional)' , ( ) => {
1147+ const result = TranslationCoverageResultSchema . parse ( {
1148+ locale : 'en' ,
1149+ totalKeys : 10 ,
1150+ translatedKeys : 10 ,
1151+ missingKeys : 0 ,
1152+ redundantKeys : 0 ,
1153+ staleKeys : 0 ,
1154+ coveragePercent : 100 ,
1155+ items : [ ] ,
1156+ } ) ;
1157+ expect ( result . breakdown ) . toBeUndefined ( ) ;
1158+ } ) ;
1159+ } ) ;
1160+
1161+ // ============================================================================
1162+ // CoverageBreakdownEntrySchema
1163+ // ============================================================================
1164+
1165+ describe ( 'CoverageBreakdownEntrySchema' , ( ) => {
1166+ it ( 'should accept a valid breakdown entry' , ( ) => {
1167+ const entry : CoverageBreakdownEntry = CoverageBreakdownEntrySchema . parse ( {
1168+ group : 'fields' ,
1169+ totalKeys : 50 ,
1170+ translatedKeys : 45 ,
1171+ coveragePercent : 90 ,
1172+ } ) ;
1173+ expect ( entry . group ) . toBe ( 'fields' ) ;
1174+ expect ( entry . totalKeys ) . toBe ( 50 ) ;
1175+ expect ( entry . translatedKeys ) . toBe ( 45 ) ;
1176+ expect ( entry . coveragePercent ) . toBe ( 90 ) ;
1177+ } ) ;
1178+
1179+ it ( 'should reject breakdown entry with negative totalKeys' , ( ) => {
1180+ expect ( ( ) =>
1181+ CoverageBreakdownEntrySchema . parse ( {
1182+ group : 'fields' ,
1183+ totalKeys : - 1 ,
1184+ translatedKeys : 0 ,
1185+ coveragePercent : 0 ,
1186+ } ) ,
1187+ ) . toThrow ( ) ;
1188+ } ) ;
1189+
1190+ it ( 'should reject breakdown entry with coverage above 100' , ( ) => {
1191+ expect ( ( ) =>
1192+ CoverageBreakdownEntrySchema . parse ( {
1193+ group : 'fields' ,
1194+ totalKeys : 10 ,
1195+ translatedKeys : 10 ,
1196+ coveragePercent : 101 ,
1197+ } ) ,
1198+ ) . toThrow ( ) ;
1199+ } ) ;
9661200} ) ;
0 commit comments