@@ -2347,6 +2347,113 @@ describe('validateTranslationReferences — flow approval steps (`approvals.<nod
23472347 } ) ;
23482348} ) ;
23492349
2350+ describe ( 'validateTranslationReferences — `page:tabs` item labels (`components.<tabsId>.items.<value>.label`)' , ( ) => {
2351+ /**
2352+ * HotCRM's measured shape: a home page whose `page:tabs` holds the three
2353+ * tabs, beside an accordion and a related list, so each verdict below is
2354+ * judged against the same metadata as the clean run and differs only in
2355+ * the bundle.
2356+ */
2357+ const homeStack = {
2358+ pages : [
2359+ {
2360+ name : 'sales_home_page' ,
2361+ label : 'Sales Home' ,
2362+ regions : [ {
2363+ name : 'main' ,
2364+ components : [
2365+ {
2366+ type : 'page:tabs' ,
2367+ id : 'home_tabs' ,
2368+ properties : {
2369+ items : [
2370+ { label : 'My Leads' , value : 'my_leads' , children : [ { type : 'record:related_list' , id : 'home_my_leads' , properties : { title : 'Leads' } } ] } ,
2371+ { label : 'My Open Deals' , value : 'my_deals' , children : [ ] } ,
2372+ { label : 'My Open Tasks' , children : [ ] } ,
2373+ ] ,
2374+ } ,
2375+ } ,
2376+ { type : 'page:accordion' , id : 'faq' , properties : { items : [ { label : 'Pricing' , children : [ ] } ] } } ,
2377+ ] ,
2378+ } ] ,
2379+ } ,
2380+ ] ,
2381+ } ;
2382+ const bundle = ( pages : unknown ) => ( { ...homeStack , translations : [ { 'zh-CN' : { pages } } ] } ) ;
2383+ const tabs = ( items : unknown , extra : Record < string , unknown > = { } ) =>
2384+ bundle ( { sales_home_page : { components : { home_tabs : { items } , ...extra } } } ) ;
2385+
2386+ it ( 'reports nothing for the label of a valued tab item' , ( ) => {
2387+ expect ( validateTranslationReferences ( tabs ( { my_leads : { label : '我的线索' } , my_deals : { label : '我的商机' } } ) ) ) . toEqual ( [ ] ) ;
2388+ } ) ;
2389+
2390+ it ( 'controls: the component keys of the same page still resolve beside it, judged by nothing new' , ( ) => {
2391+ expect (
2392+ validateTranslationReferences (
2393+ tabs ( { my_leads : { label : '我的线索' } } , { home_my_leads : { title : '线索' } , faq : { label : '常见问题' } } ) ,
2394+ ) ,
2395+ ) . toEqual ( [ ] ) ;
2396+ } ) ;
2397+
2398+ it ( 'refuses an item key over an UNKNOWN value, with the nearest tab value offered' , ( ) => {
2399+ const findings = validateTranslationReferences ( tabs ( { my_lead : { label : 'x' } } ) ) ;
2400+ expect ( findings ) . toHaveLength ( 1 ) ;
2401+ expect ( findings [ 0 ] . rule ) . toBe ( TRANSLATION_TARGET_UNKNOWN ) ;
2402+ expect ( findings [ 0 ] . severity ) . toBe ( 'error' ) ;
2403+ expect ( findings [ 0 ] . path ) . toBe ( 'translations[0]["zh-CN"].pages.sales_home_page.components.home_tabs.items.my_lead' ) ;
2404+ expect ( findings [ 0 ] . message ) . toBe (
2405+ 'Translations are keyed to tab "my_lead", which `page:tabs` "home_tabs" on page "sales_home_page" gives ' +
2406+ 'no item as its `value`, so the tab keeps its source-locale label. Did you mean "my_leads"?' ,
2407+ ) ;
2408+ expect ( findings [ 0 ] . hint ) . toBe (
2409+ "Tab-item translations are keyed by the item's `value`; give the item a `value` if it has none." ,
2410+ ) ;
2411+ } ) ;
2412+
2413+ it ( 'refuses an item key over an index token, which no key ever reaches' , ( ) => {
2414+ const findings = validateTranslationReferences ( tabs ( { 'tab-2' : { label : 'x' } } ) ) ;
2415+ expect ( findings ) . toHaveLength ( 1 ) ;
2416+ expect ( findings [ 0 ] . path ) . toBe ( 'translations[0]["zh-CN"].pages.sales_home_page.components.home_tabs.items.tab-2' ) ;
2417+ } ) ;
2418+
2419+ it ( 'refuses `items` on a `page:accordion`, pointing at the inline locale map' , ( ) => {
2420+ const findings = validateTranslationReferences ( bundle ( { sales_home_page : { components : { faq : { items : { pricing : { label : 'x' } } } } } } ) ) ;
2421+ expect ( findings ) . toHaveLength ( 1 ) ;
2422+ expect ( findings [ 0 ] . path ) . toBe ( 'translations[0]["zh-CN"].pages.sales_home_page.components.faq.items' ) ;
2423+ expect ( findings [ 0 ] . message ) . toBe (
2424+ 'Translations carry tab items for "faq", which page "sales_home_page" declares as a `page:accordion`, ' +
2425+ 'whose panels have no stable key, so nothing resolves them.' ,
2426+ ) ;
2427+ expect ( findings [ 0 ] . hint ) . toBe ( 'Localize an accordion panel label in place, as an inline locale map on `items[].label`.' ) ;
2428+ } ) ;
2429+
2430+ it ( 'refuses `items` on a component of another type, by name' , ( ) => {
2431+ const findings = validateTranslationReferences (
2432+ bundle ( { sales_home_page : { components : { home_my_leads : { items : { leads : { label : 'x' } } } } } } ) ,
2433+ ) ;
2434+ expect ( findings ) . toHaveLength ( 1 ) ;
2435+ expect ( findings [ 0 ] . message ) . toBe (
2436+ 'Translations carry tab items for "home_my_leads", which page "sales_home_page" declares as a ' +
2437+ '`record:related_list`, not a `page:tabs`, so nothing resolves them.' ,
2438+ ) ;
2439+ expect ( findings [ 0 ] . hint ) . toBe ( 'Tab-item translations go under the `id` of a `page:tabs` component.' ) ;
2440+ } ) ;
2441+
2442+ it ( 'refuses `items` under a component id the page does not declare' , ( ) => {
2443+ const findings = validateTranslationReferences ( bundle ( { sales_home_page : { components : { home_tab : { items : { my_leads : { label : 'x' } } } } } } ) ) ;
2444+ expect ( findings ) . toHaveLength ( 1 ) ;
2445+ expect ( findings [ 0 ] . path ) . toBe ( 'translations[0]["zh-CN"].pages.sales_home_page.components.home_tab.items' ) ;
2446+ expect ( findings [ 0 ] . message ) . toBe (
2447+ 'Translations carry tab items for "home_tab", a component id page "sales_home_page" does not declare, ' +
2448+ 'so nothing resolves them. Did you mean "home_tabs"?' ,
2449+ ) ;
2450+ } ) ;
2451+
2452+ it ( 'control: a page this stack does not declare is not judged by this check' , ( ) => {
2453+ expect ( validateTranslationReferences ( bundle ( { other_page : { components : { t : { items : { a : { label : 'x' } } } } } } ) ) ) . toEqual ( [ ] ) ;
2454+ } ) ;
2455+ } ) ;
2456+
23502457describe ( 'validateTranslationReferences — flow toasts and screen-field options (#22507)' , ( ) => {
23512458 /**
23522459 * hotclm's measured shape, trimmed: a screen flow that authors a completion
@@ -4035,6 +4142,10 @@ describe('[#22161] one-line verdicts — translation-target-unknown, translation
40354142 '`resultDialog.fields` does not declare' ,
40364143 'declares no `fields` (it renders the whole response as JSON)' ,
40374144 'Translations are keyed to picklist "' ,
4145+ 'gives no item as its `value`' ,
4146+ 'whose panels have no stable key' ,
4147+ 'not a `page:tabs`, so nothing resolves them' ,
4148+ 'a component id page "' ,
40384149 ] ;
40394150 const OPTION_ARMS = [
40404151 'which declares no `options` at all (field type' ,
@@ -4186,6 +4297,9 @@ describe('[#22161] one-line fixes — translation-target-unknown', () => {
41864297 'Move the `resultDialog` translations under the action' ,
41874298 "Match the key to a declared field's literal `path`" ,
41884299 "Match the key to a picklist's `name`" ,
4300+ "Tab-item translations are keyed by the item's `value`" ,
4301+ 'Localize an accordion panel label in place' ,
4302+ 'Tab-item translations go under the `id` of a `page:tabs` component' ,
41894303 ] ;
41904304
41914305 it ( 'every `fix:` line the cases above fired is one line of at most 200 characters' , ( ) => {
0 commit comments