@@ -7176,6 +7176,139 @@ const elementFormRemoved: MetadataConversion = {
71767176 } ,
71777177} ;
71787178
7179+ /**
7180+ * `translation.pages.<name>.components.<id>.submitLabel` — the component-copy
7181+ * key retired with its only declarer (protocol 18, #10926, ADR-0049).
7182+ *
7183+ * The face is measured, not mirrored: each copy key exists because some
7184+ * component in `ComponentPropsMap` declares it, and `submitLabel`'s only
7185+ * declarer was `element:form` — retired whole by #9249 (`element-form-removed`
7186+ * above). The maintainer ruled retire over re-anchor (#10926): the live form
7187+ * surface's submit copy is `object-form`'s `submitText` (`I18nLabelSchema`),
7188+ * localizable at its own authoring site, so re-anchoring would have widened
7189+ * the face for one word. The key, its `submit` alias and its
7190+ * `PAGE_COMPONENT_COPY_KEYS` slot are gone; the schema rejection carries the
7191+ * prescription.
7192+ *
7193+ * Pure lossless delete — since #9249 no resolver overlaid the key, so a stored
7194+ * translation kept a string nothing read. Both authored shapes are walked: a
7195+ * bundle entry (locale → data map, `stack.translations`' declared shape) and a
7196+ * bare data/item entry (groups at the top level — the shape stored `translation`
7197+ * items replay through, the `translation-validation-messages-removed`
7198+ * precedent).
7199+ */
7200+ const translationComponentSubmitLabelRemoved : MetadataConversion = {
7201+ id : 'translation-component-submit-label-removed' ,
7202+ toMajor : 18 ,
7203+ retiredFromLoadPath : true ,
7204+ surface : 'translation.pages.components.submitLabel' ,
7205+ summary :
7206+ "translation component-copy key 'submitLabel' removed (#10926 — its only declared carrier, "
7207+ + "'element:form', retired whole in #9249, so the resolver no longer overlays it and a stored "
7208+ + "string was read by nothing; the live form surface's submit copy is 'object-form''s "
7209+ + "'submitText', localized at its own authoring site)" ,
7210+ apply ( stack , emit ) {
7211+ const stripFromData = ( data : Record < string , unknown > , path : string ) : Record < string , unknown > => {
7212+ const pages = data . pages ;
7213+ if ( ! isDict ( pages ) ) return data ;
7214+ let pagesChanged = false ;
7215+ const nextPages : Record < string , unknown > = { ...pages } ;
7216+ for ( const [ pageName , page ] of Object . entries ( pages ) ) {
7217+ if ( ! isDict ( page ) || ! isDict ( page . components ) ) continue ;
7218+ let componentsChanged = false ;
7219+ const nextComponents : Record < string , unknown > = { ...page . components } ;
7220+ for ( const [ id , entry ] of Object . entries ( page . components ) ) {
7221+ if ( ! isDict ( entry ) ) continue ;
7222+ const stripped = stripKeys ( entry , [ 'submitLabel' ] , emit , `${ path } .pages.${ pageName } .components.${ id } ` ) ;
7223+ if ( stripped === entry ) continue ;
7224+ nextComponents [ id ] = stripped ;
7225+ componentsChanged = true ;
7226+ }
7227+ if ( ! componentsChanged ) continue ;
7228+ nextPages [ pageName ] = { ...page , components : nextComponents } ;
7229+ pagesChanged = true ;
7230+ }
7231+ return pagesChanged ? { ...data , pages : nextPages } : data ;
7232+ } ;
7233+ return mapCollection ( stack , 'translations' , ( entry , path ) => {
7234+ // Bare data/item shape: the groups sit at the entry's top level.
7235+ let next = stripFromData ( entry , path ) ;
7236+ // Bundle shape: locale code → data. Judged structurally (a dict whose
7237+ // `pages` is a dict) rather than by key spelling — `LocaleSchema` is an
7238+ // open string, so the locale keys cannot be enumerated. A strip-only
7239+ // walk makes a false positive a no-op: it removes nothing unless the
7240+ // exact `pages.<name>.components.<id>.submitLabel` path is present.
7241+ for ( const [ locale , data ] of Object . entries ( next ) ) {
7242+ if ( ! isDict ( data ) || ! isDict ( data . pages ) ) continue ;
7243+ const stripped = stripFromData ( data , `${ path } .${ locale } ` ) ;
7244+ if ( stripped === data ) continue ;
7245+ next = next === entry ? { ...entry } : next ;
7246+ next [ locale ] = stripped ;
7247+ }
7248+ return next ;
7249+ } ) ;
7250+ } ,
7251+ fixture : {
7252+ before : {
7253+ translations : [
7254+ {
7255+ // The bundle shape `stack.translations` declares.
7256+ 'zh-CN' : {
7257+ pages : {
7258+ sales_home_page : {
7259+ components : {
7260+ new_lead_form : { submitLabel : '创建' } ,
7261+ // Live keys on a neighbor ride through untouched.
7262+ quick_create : { title : '快速新建' } ,
7263+ } ,
7264+ } ,
7265+ } ,
7266+ } ,
7267+ } ,
7268+ {
7269+ // The bare item shape stored `translation` rows replay through.
7270+ name : 'ja_jp' ,
7271+ locale : 'ja-JP' ,
7272+ pages : {
7273+ sales_home_page : {
7274+ components : { new_lead_form : { submitLabel : '作成' } } ,
7275+ } ,
7276+ } ,
7277+ } ,
7278+ ] ,
7279+ } ,
7280+ after : {
7281+ translations : [
7282+ {
7283+ 'zh-CN' : {
7284+ pages : {
7285+ sales_home_page : {
7286+ components : {
7287+ new_lead_form : { } ,
7288+ quick_create : { title : '快速新建' } ,
7289+ } ,
7290+ } ,
7291+ } ,
7292+ } ,
7293+ } ,
7294+ {
7295+ name : 'ja_jp' ,
7296+ locale : 'ja-JP' ,
7297+ pages : {
7298+ sales_home_page : {
7299+ components : { new_lead_form : { } } ,
7300+ } ,
7301+ } ,
7302+ } ,
7303+ ] ,
7304+ } ,
7305+ // One per stripped key instance: one in the bundle-shaped entry, one in
7306+ // the item-shaped entry. The emptied component bag stays — the conversion
7307+ // strips KEYS, and deleting the bag would be a second, unprescribed edit.
7308+ expectedNotices : 2 ,
7309+ } ,
7310+ } ;
7311+
71797312/**
71807313 * `field.inlineColumns[]` / `field.relatedListColumns[]` — the mechanical half
71817314 * of the #9227 strict-element narrowing (protocol 18).
@@ -7752,6 +7885,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly<Record<number, readonly MetadataConv
77527885 metricFiltersRemoved ,
77537886 recordHighlightsFieldIconRemoved ,
77547887 mappingLookupParamsRemoved ,
7888+ translationComponentSubmitLabelRemoved ,
77557889 ] ,
77567890} ;
77577891
0 commit comments