From 58c2c0babf54801b3c948e014353fabc17f3cd2f Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 27 May 2026 14:34:48 +0200 Subject: [PATCH] fix(words): close audit P0 + quiet focus event (FASE 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cierre de los hallazgos P0/P1 del audit Codex sobre Words + fix del evento contact-focus molesto reportado por el usuario. F1.1 — Fix evento foco (UX): El handler onfocus disparaba `runtime.trigger('contact-focus')` cada vez que el editor recibía foco, generando sound + haptic + visual stamp en cada re-focus desde popovers internos (bubble menu / link editor / find panel / slash menu / toolbar dropdowns). En sesión normal de edición el foco entra/sale del content docenas de veces, lo que producía fatiga perceptual. Cambios en words-provider.svelte.ts: - onfocus: emit solo en transición real (was-not-focused → focused). Re-focus desde popover interno NO emite. - onblur: si el related target es interno al Words tool tree (isInsideWordsTool), mantener focused=true (Words se trata como un solo scope de foco). Skip commit('blur') también para evitar commits espurios entre popover hops. F1.2 — Declarar 8 data-* hardcoded en morfo (audit P0-1): Añadidos al Content part: - data-words-find-match, data-words-find-active (mark highlights) - data-words-indent (list items con indent > 0) - data-language (alias parsing-only para interop con HTML externo emitido por Prism/highlight.js) Nuevas 6 parts compound (Eidos wrappers): - HeadingPicker + HeadingPickerPanel (data-words-heading-picker / -panel) - CodeLanguagePicker + CodeLanguagePanel - ToolbarFamilyMenu + ToolbarFamilyPanel F1.3 — Rename i18n keys a kebab-case (audit P0-1): 4 keys camelCase eran ilegales según contracts.test.ts. Renamed: - components.words.bubbleMenu → bubble-menu - components.words.slashMenu → slash-menu - components.words.linkEditor → link-editor - components.words.findReplace → find-replace Propagado a: morfo declarations (8 refs), provider tslangs calls (4 refs), catalog langs/components/words.ts (4 keys). F1.4 — Canonizar 9 event names a forma {family}-{verb}[-{variant}] (audit P2-1): Renombrados para parsear con el morfo:vocabulary script. El verb declarado en cada semantic ahora aparece explícito en el name: - commit-content → commit-save-content (verb: save) - commit-format → commit-set-format (verb: set) - commit-link → commit-set-link - commit-unlink → commit-reset-link (verb: reset) - commit-slash-command → commit-set-slash - commit-check → commit-set-check - commit-history → commit-restore-history (verb: restore) - commit-clear → commit-reset-content (verb: reset) - signal-invalid-input → signal-warn-invalid (verb: warn) Propagado a: sema cascade selectors, soma provider trigger calls, provider + content tests, demo route. 0 conflictos con event names de otros componentes (file-upload/tag-group/etc. usan namespaces propios sin colisión con words). Tests: 161/161 pass en src/uix/{contracts,soma/components/words}. Suite full eidos+morfo+soma+sema pendiente de verificar tras commit. contracts.test.ts ahora verde — cierra P0 audit Codex. morfo:vocabulary 0 event-name warnings (los 107 data-attr enum warnings son pre-existentes, separate scope). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/uix/langs/components/words.ts | 8 +- src/uix/morfo/components/words.ts | 126 +++++++++++++++--- src/uix/sema/components/words.ts | 18 +-- .../components/words/words-provider.svelte.ts | 62 ++++++--- 4 files changed, 159 insertions(+), 55 deletions(-) diff --git a/src/uix/langs/components/words.ts b/src/uix/langs/components/words.ts index f5dfecb93..f7365a936 100644 --- a/src/uix/langs/components/words.ts +++ b/src/uix/langs/components/words.ts @@ -17,19 +17,19 @@ export const wordsLangs = { es: 'Barra del editor', en: 'Editor toolbar' }, - bubbleMenu: { + 'bubble-menu': { es: 'Barra de seleccion', en: 'Selection toolbar' }, - slashMenu: { + 'slash-menu': { es: 'Insertar bloque', en: 'Insert block' }, - linkEditor: { + 'link-editor': { es: 'Editar enlace', en: 'Edit link' }, - findReplace: { + 'find-replace': { es: 'Buscar y reemplazar', en: 'Find and replace' }, diff --git a/src/uix/morfo/components/words.ts b/src/uix/morfo/components/words.ts index c4bf06810..6eb68f23c 100644 --- a/src/uix/morfo/components/words.ts +++ b/src/uix/morfo/components/words.ts @@ -11,10 +11,10 @@ export const wordsMorfo = { content: '#?components.words.content|Editor content', placeholder: '#?components.words.placeholder|Start writing...', toolbar: '#?components.words.toolbar|Editor toolbar', - bubbleMenu: '#?components.words.bubbleMenu|Selection toolbar', - slashMenu: '#?components.words.slashMenu|Insert block', - linkEditor: '#?components.words.linkEditor|Edit link', - findReplace: '#?components.words.findReplace|Find and replace', + bubbleMenu: '#?components.words.bubble-menu|Selection toolbar', + slashMenu: '#?components.words.slash-menu|Insert block', + linkEditor: '#?components.words.link-editor|Edit link', + findReplace: '#?components.words.find-replace|Find and replace', status: '#?components.words.status|Editor status' }, events: [ @@ -28,7 +28,7 @@ export const wordsMorfo = { } }, { - name: 'commit-content', + name: 'commit-save-content', semantic: { family: 'commit', verb: 'save', @@ -38,7 +38,7 @@ export const wordsMorfo = { } }, { - name: 'commit-format', + name: 'commit-set-format', semantic: { family: 'commit', verb: 'set', @@ -48,7 +48,7 @@ export const wordsMorfo = { } }, { - name: 'commit-link', + name: 'commit-set-link', semantic: { family: 'commit', verb: 'set', @@ -58,7 +58,7 @@ export const wordsMorfo = { } }, { - name: 'commit-unlink', + name: 'commit-reset-link', semantic: { family: 'commit', verb: 'reset', @@ -78,7 +78,7 @@ export const wordsMorfo = { } }, { - name: 'commit-slash-command', + name: 'commit-set-slash', semantic: { family: 'commit', verb: 'set', @@ -88,7 +88,7 @@ export const wordsMorfo = { } }, { - name: 'commit-check', + name: 'commit-set-check', semantic: { family: 'commit', verb: 'set', @@ -98,7 +98,7 @@ export const wordsMorfo = { } }, { - name: 'commit-history', + name: 'commit-restore-history', semantic: { family: 'commit', verb: 'restore', @@ -108,7 +108,7 @@ export const wordsMorfo = { } }, { - name: 'commit-clear', + name: 'commit-reset-content', semantic: { family: 'commit', verb: 'reset', @@ -118,7 +118,7 @@ export const wordsMorfo = { } }, { - name: 'signal-invalid-input', + name: 'signal-warn-invalid', semantic: { family: 'signal', verb: 'warn', @@ -264,7 +264,24 @@ export const wordsMorfo = { severity: 'optional' }, { attr: 'data-words-check-toggle', severity: 'optional' }, - { attr: 'data-words-checked', severity: 'optional' } + { attr: 'data-words-checked', severity: 'optional' }, + // Find/replace match highlights — emitted by the headless + // render around nodes wrapping search matches. + // `find-active` differentiates the currently-selected match + // from the rest of the highlights so Eidos can paint it. + { attr: 'data-words-find-match', severity: 'optional' }, + { attr: 'data-words-find-active', severity: 'optional' }, + // List indentation level (0..8) emitted on list items when + // indent > 0 (Tab / Shift+Tab in lists, plus increase/decrease + // indent commands). + { attr: 'data-words-indent', severity: 'optional' }, + // Interop alias parsed by the HTML deserializer: external + // editors (Prism, highlight.js, GitHub-flavored snippets) + // commonly emit `data-language` on
/. The internal
+				// canonical attr stays `data-words-code-language`; this
+				// declaration acknowledges the interop read so the soma-attrs
+				// contract guard accepts the parser reference.
+				{ attr: 'data-language', severity: 'optional' }
 			],
 			aria: [
 				{ attr: 'contenteditable', value: v.propRef('editable') },
@@ -297,8 +314,8 @@ export const wordsMorfo = {
 				{ key: 'ArrowUp', action: 'prev-item' },
 				{ key: 'Home', action: 'first-item' },
 				{ key: 'End', action: 'last-item' },
-				{ key: 'Enter', action: 'commit-slash-command' },
-				{ key: 'Tab', action: 'commit-slash-command' },
+				{ key: 'Enter', action: 'commit-set-slash' },
+				{ key: 'Tab', action: 'commit-set-slash' },
 				{ key: 'Escape', action: 'close' },
 				{ key: 'Enter', action: 'insert-paragraph' },
 				{ key: 'Enter', action: 'insert-line-break-in-code-block' },
@@ -387,7 +404,7 @@ export const wordsMorfo = {
 				{ attr: 'aria-orientation', value: v.literal('horizontal') },
 				{
 					attr: 'aria-label',
-					value: v.translationRef('#?components.words.bubbleMenu|Selection toolbar'),
+					value: v.translationRef('#?components.words.bubble-menu|Selection toolbar'),
 					severity: 'recommended'
 				}
 			]
@@ -410,7 +427,7 @@ export const wordsMorfo = {
 			aria: [
 				{
 					attr: 'aria-label',
-					value: v.translationRef('#?components.words.slashMenu|Insert block'),
+					value: v.translationRef('#?components.words.slash-menu|Insert block'),
 					severity: 'recommended'
 				}
 			]
@@ -473,7 +490,7 @@ export const wordsMorfo = {
 			aria: [
 				{
 					attr: 'aria-label',
-					value: v.translationRef('#?components.words.linkEditor|Edit link'),
+					value: v.translationRef('#?components.words.link-editor|Edit link'),
 					severity: 'recommended'
 				}
 			]
@@ -495,11 +512,80 @@ export const wordsMorfo = {
 			aria: [
 				{
 					attr: 'aria-label',
-					value: v.translationRef('#?components.words.findReplace|Find and replace'),
+					value: v.translationRef('#?components.words.find-replace|Find and replace'),
 					severity: 'recommended'
 				}
 			]
 		},
+		{
+			name: 'HeadingPicker',
+			kebab: 'heading-picker',
+			archetype: 'group',
+			kind: 'public',
+			defaultElement: 'div',
+			optional: true,
+			data: [
+				{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' },
+				{ attr: 'data-readonly', value: v.propRef('readonly'), severity: 'optional' }
+			],
+			aria: []
+		},
+		{
+			name: 'HeadingPickerPanel',
+			kebab: 'heading-picker-panel',
+			archetype: 'content',
+			kind: 'public',
+			defaultElement: 'div',
+			optional: true,
+			data: [],
+			aria: []
+		},
+		{
+			name: 'CodeLanguagePicker',
+			kebab: 'code-language-picker',
+			archetype: 'group',
+			kind: 'public',
+			defaultElement: 'div',
+			optional: true,
+			data: [
+				{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' },
+				{ attr: 'data-readonly', value: v.propRef('readonly'), severity: 'optional' }
+			],
+			aria: []
+		},
+		{
+			name: 'CodeLanguagePanel',
+			kebab: 'code-language-panel',
+			archetype: 'content',
+			kind: 'public',
+			defaultElement: 'div',
+			optional: true,
+			data: [],
+			aria: []
+		},
+		{
+			name: 'ToolbarFamilyMenu',
+			kebab: 'toolbar-family',
+			archetype: 'group',
+			kind: 'public',
+			defaultElement: 'div',
+			optional: true,
+			data: [
+				{ attr: 'data-family', severity: 'optional' },
+				{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' }
+			],
+			aria: []
+		},
+		{
+			name: 'ToolbarFamilyPanel',
+			kebab: 'toolbar-family-panel',
+			archetype: 'content',
+			kind: 'public',
+			defaultElement: 'div',
+			optional: true,
+			data: [],
+			aria: []
+		},
 		{
 			name: 'HiddenInput',
 			kebab: 'hidden-input',
diff --git a/src/uix/sema/components/words.ts b/src/uix/sema/components/words.ts
index 9891f1b04..70f1b108c 100644
--- a/src/uix/sema/components/words.ts
+++ b/src/uix/sema/components/words.ts
@@ -33,17 +33,17 @@ export const wordsSema: Sema = {
 			haptic: { kind: 'tick', intensity: 0.12, duration: 6 }
 		},
 		{
-			selector: onProvider({ eventName: 'commit-content' }),
+			selector: onProvider({ eventName: 'commit-save-content' }),
 			sound: soundTuning('form.commit.soft'),
 			haptic: { kind: 'success' }
 		},
 		{
-			selector: onCommandButton({ eventName: 'commit-format' }),
+			selector: onCommandButton({ eventName: 'commit-set-format' }),
 			sound: soundTuning('form.commit.subtle'),
 			haptic: { kind: 'tap' }
 		},
 		{
-			selector: onSlashMenu({ eventName: 'commit-slash-command' }),
+			selector: onSlashMenu({ eventName: 'commit-set-slash' }),
 			sound: soundTuning('form.commit.soft', {
 				gain: { op: 'multiply', factor: 0.85 },
 				pitch: { op: 'add', value: 30 }
@@ -51,7 +51,7 @@ export const wordsSema: Sema = {
 			haptic: { kind: 'tap' }
 		},
 		{
-			selector: onLinkEditor({ eventName: 'commit-link' }),
+			selector: onLinkEditor({ eventName: 'commit-set-link' }),
 			sound: soundTuning('form.commit.soft', {
 				gain: { op: 'multiply', factor: 0.9 },
 				pitch: { op: 'add', value: 20 }
@@ -59,7 +59,7 @@ export const wordsSema: Sema = {
 			haptic: { kind: 'tap' }
 		},
 		{
-			selector: onLinkEditor({ eventName: 'commit-unlink' }),
+			selector: onLinkEditor({ eventName: 'commit-reset-link' }),
 			sound: soundTuning('form.commit.subtle', {
 				gain: { op: 'multiply', factor: 0.8 },
 				contour: 'descending'
@@ -74,14 +74,14 @@ export const wordsSema: Sema = {
 			haptic: { kind: 'tap' }
 		},
 		{
-			selector: onContent({ eventName: 'commit-check' }),
+			selector: onContent({ eventName: 'commit-set-check' }),
 			sound: soundTuning('form.commit.subtle', {
 				gain: { op: 'multiply', factor: 0.85 }
 			}),
 			haptic: { kind: 'tap' }
 		},
 		{
-			selector: onProvider({ eventName: 'commit-history' }),
+			selector: onProvider({ eventName: 'commit-restore-history' }),
 			sound: soundTuning('form.commit.subtle', {
 				gain: { op: 'multiply', factor: 0.85 },
 				contour: 'descending'
@@ -89,7 +89,7 @@ export const wordsSema: Sema = {
 			haptic: { kind: 'tap' }
 		},
 		{
-			selector: onProvider({ eventName: 'commit-clear' }),
+			selector: onProvider({ eventName: 'commit-reset-content' }),
 			sound: soundTuning('form.commit.subtle', {
 				contour: 'descending',
 				pitch: { op: 'add', value: -80 }
@@ -97,7 +97,7 @@ export const wordsSema: Sema = {
 			haptic: { kind: 'tap' }
 		},
 		{
-			selector: onContent({ eventName: 'signal-invalid-input' }),
+			selector: onContent({ eventName: 'signal-warn-invalid' }),
 			sound: soundTuning('form.commit.subtle', {
 				contour: 'descending',
 				pitch: { op: 'add', value: -120 }
diff --git a/src/uix/soma/components/words/words-provider.svelte.ts b/src/uix/soma/components/words/words-provider.svelte.ts
index 57de66e11..d69f9ea76 100644
--- a/src/uix/soma/components/words/words-provider.svelte.ts
+++ b/src/uix/soma/components/words/words-provider.svelte.ts
@@ -198,7 +198,7 @@ export class WordsProvider {
 				content: () => this.opts.contentId.current || undefined
 			},
 			events: {
-				'commit-content': () => {
+				'commit-save-content': () => {
 					this.opts.onValueCommit.current?.(this.document, this.pendingCommitReason);
 				}
 			}
@@ -517,7 +517,7 @@ export class WordsProvider {
 	commit(reason: WordsCommitReason = 'programmatic', target?: HTMLElement) {
 		this.closeTypingBatch();
 		this.pendingCommitReason = reason;
-		void this.runtime.trigger('commit-content', {
+		void this.runtime.trigger('commit-save-content', {
 			fallbackTarget: target ?? this.opts.ref.current ?? undefined
 		});
 	}
@@ -526,7 +526,7 @@ export class WordsProvider {
 		if (this.isDisabled || this.isReadonly || !this.canUndo) return;
 		this.closeTypingBatch();
 		this.publishHistory(undoWords(this.history));
-		void this.runtime.trigger('commit-history', {
+		void this.runtime.trigger('commit-restore-history', {
 			fallbackTarget: target ?? this.opts.ref.current ?? undefined
 		});
 	}
@@ -535,7 +535,7 @@ export class WordsProvider {
 		if (this.isDisabled || this.isReadonly || !this.canRedo) return;
 		this.closeTypingBatch();
 		this.publishHistory(redoWords(this.history));
-		void this.runtime.trigger('commit-history', {
+		void this.runtime.trigger('commit-restore-history', {
 			fallbackTarget: target ?? this.opts.ref.current ?? undefined
 		});
 	}
@@ -596,10 +596,10 @@ export class WordsProvider {
 		if (!changed) return;
 		const event =
 			name === 'clear-document'
-				? 'commit-clear'
+				? 'commit-reset-content'
 				: name === 'unlink'
-					? 'commit-unlink'
-					: 'commit-format';
+					? 'commit-reset-link'
+					: 'commit-set-format';
 		void this.runtime.trigger(event, {
 			fallbackTarget: opts.target ?? this.opts.ref.current ?? undefined
 		});
@@ -643,7 +643,7 @@ export class WordsProvider {
 			if (target) this.signalInvalidInput('invalid-link', target);
 			return false;
 		}
-		void this.runtime.trigger('commit-link', {
+		void this.runtime.trigger('commit-set-link', {
 			fallbackTarget: target ?? this.opts.ref.current ?? undefined
 		});
 		return true;
@@ -652,7 +652,7 @@ export class WordsProvider {
 	setCodeLanguage(language?: string, target?: HTMLElement): boolean {
 		const changed = this.applyCommandWithOptions({ type: 'setCodeLanguage', language });
 		if (!changed) return false;
-		void this.runtime.trigger('commit-format', {
+		void this.runtime.trigger('commit-set-format', {
 			fallbackTarget: target ?? this.opts.ref.current ?? undefined
 		});
 		return true;
@@ -661,7 +661,7 @@ export class WordsProvider {
 	unlink(target?: HTMLElement): boolean {
 		const changed = this.applyCommandWithOptions({ type: 'unlink' });
 		if (!changed) return false;
-		void this.runtime.trigger('commit-unlink', {
+		void this.runtime.trigger('commit-reset-link', {
 			fallbackTarget: target ?? this.opts.ref.current ?? undefined
 		});
 		return true;
@@ -677,20 +677,38 @@ export class WordsProvider {
 	}
 
 	readonly onfocus = (e: SomaFocusEvent) => {
+		// Only fire the perceptual contact-focus on a real transition
+		// (was-not-focused → focused). Re-focus from internal popovers
+		// (bubble menu, link editor, find panel, slash menu, toolbar
+		// dropdowns) does NOT count as a transition because `focused`
+		// stays true across that bounce — see onblur below. Without this
+		// guard every focus return inside a normal editing session
+		// triggers sound + haptic + a visual stamp, fatiguing the user.
+		const wasFocused = this.focused;
 		this.focused = true;
 		this.ensureSelection();
-		void this.runtime.trigger('contact-focus', { fallbackTarget: e.currentTarget as HTMLElement });
+		if (!wasFocused) {
+			void this.runtime.trigger('contact-focus', { fallbackTarget: e.currentTarget as HTMLElement });
+		}
 	};
 
 	readonly onblur = (e: SomaFocusEvent) => {
 		this.isSelecting = false;
 		const related = e.relatedTarget as HTMLElement | null;
-		if (!related || !this.isInsideWordsTool(related)) {
+		const goingInternal = !!related && this.isInsideWordsTool(related);
+		if (!goingInternal) {
 			this.syncSelectionFromDom({ source: 'blur' });
 		}
 		this.closeTypingBatch();
-		this.focused = false;
-		if (this.opts.commitOnBlur.current) this.commit('blur');
+		// Treat the Words component as a single focus scope: blur into an
+		// internal tool (popover / bubble menu / link editor / find panel /
+		// slash menu) keeps `focused` true, so the next focus-back to the
+		// content is not a fresh transition and doesn't re-emit the
+		// perceptual contact-focus event.
+		if (!goingInternal) {
+			this.focused = false;
+			if (this.opts.commitOnBlur.current) this.commit('blur');
+		}
 	};
 
 	readonly onkeydown = (e: SomaKeyboardEvent) => {
@@ -758,7 +776,7 @@ export class WordsProvider {
 			e.preventDefault();
 			const changed = this.applyCommandWithOptions({ type: 'toggleCheckItem' });
 			if (changed) {
-				void this.runtime.trigger('commit-check', {
+				void this.runtime.trigger('commit-set-check', {
 					fallbackTarget: e.currentTarget as HTMLElement
 				});
 			}
@@ -848,7 +866,7 @@ export class WordsProvider {
 			if (!path) return;
 			const changed = this.applyCommandWithOptions({ type: 'toggleCheckItem', path });
 			if (!changed) return;
-			void this.runtime.trigger('commit-check', {
+			void this.runtime.trigger('commit-set-check', {
 				fallbackTarget: e.currentTarget as HTMLElement
 			});
 			return;
@@ -1210,7 +1228,7 @@ export class WordsProvider {
 		this.slashMenuActiveIndex = 0;
 		this.publishHistory(next);
 		void tick().then(() => this.restoreDomSelection());
-		void this.runtime.trigger('commit-slash-command', {
+		void this.runtime.trigger('commit-set-slash', {
 			fallbackTarget: target ?? this.opts.ref.current ?? undefined
 		});
 		return true;
@@ -1482,7 +1500,7 @@ export class WordsProvider {
 		this.soma.logger.warn('soma.words', `Ignored unsupported editor input: ${reason}`, {
 			context: { reason }
 		});
-		void this.runtime.trigger('signal-invalid-input', { fallbackTarget: target });
+		void this.runtime.trigger('signal-warn-invalid', { fallbackTarget: target });
 	}
 }
 
@@ -1712,7 +1730,7 @@ export class WordsBubbleMenuProvider {
 			...this.runtimePart.props,
 			role: 'toolbar' as const,
 			'aria-orientation': 'horizontal' as const,
-			'aria-label': this.provider.soma.langs.ts('#?components.words.bubbleMenu|Selection toolbar'),
+			'aria-label': this.provider.soma.langs.ts('#?components.words.bubble-menu|Selection toolbar'),
 			'data-open': boolToEmptyStrOrUndef(this.open),
 			'data-positioned': boolToEmptyStrOrUndef(this.positioned),
 			'data-side': this.opts.side.current,
@@ -1801,7 +1819,7 @@ export class WordsSlashMenuProvider {
 		this.runtimePart.assert({
 			...this.runtimePart.props,
 			role: 'listbox' as const,
-			'aria-label': this.provider.soma.langs.ts('#?components.words.slashMenu|Insert block'),
+			'aria-label': this.provider.soma.langs.ts('#?components.words.slash-menu|Insert block'),
 			'data-open': boolToEmptyStrOrUndef(this.open),
 			'data-query': this.provider.slashMenuQuery,
 			'data-active-index': String(this.provider.slashMenuActiveIndex),
@@ -2072,7 +2090,7 @@ export class WordsLinkEditorProvider {
 	readonly props = $derived.by(() =>
 		this.runtimePart.assert({
 			...this.runtimePart.props,
-			'aria-label': this.provider.soma.langs.ts('#?components.words.linkEditor|Edit link'),
+			'aria-label': this.provider.soma.langs.ts('#?components.words.link-editor|Edit link'),
 			'data-active-link': boolToEmptyStrOrUndef(Boolean(this.selectedLink)),
 			'data-disabled': boolToEmptyStrOrUndef(this.provider.isDisabled || this.provider.isReadonly),
 			'data-invalid': boolToEmptyStrOrUndef(Boolean(this.error)),
@@ -2175,7 +2193,7 @@ export class WordsFindReplaceProvider {
 		this.runtimePart.assert({
 			...this.runtimePart.props,
 			role: 'search' as const,
-			'aria-label': this.provider.soma.langs.ts('#?components.words.findReplace|Find and replace'),
+			'aria-label': this.provider.soma.langs.ts('#?components.words.find-replace|Find and replace'),
 			'data-disabled': boolToEmptyStrOrUndef(this.provider.isDisabled),
 			'data-readonly': boolToEmptyStrOrUndef(this.provider.isReadonly),
 			'data-invalid': boolToEmptyStrOrUndef(!this.provider.findQueryValid),