docs(reconciliation): translations: -> texts: swept across morfo/soma docs

Phase 1a of PLAN-docs-reconciliation (fable_audit D1). The Morfo field is
texts (idlangrefs); the multilingual catalog lives in langs/components/*.
Fixed every example and mechanism claim that still documented the removed
translations: field: morfo/README (contains-list, anatomy, Step 4.5),
SOMA_ARCHITECTURE (Texto funcional + checklist), soma/README (morfo field
list + runtime registration), COMPONENT_GUIDE (A1/A3 + common mistakes +
smoke fixes), COMPONENT_COMPLETION_CHECKLIST M-1.3, active-uix/README,
uix/README, 4 component READMEs, and 3 stale JSDoc blocks in code
(morfo/types.ts, morfo/schema.ts invariant 7, active-uix/types.ts
registerDefaultLangs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 3095065f0c
commit 925508c764

@ -39,7 +39,7 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete.
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| M-1.1 | Exports a single `{Name}Morfo` const satisfying `Morfo` | error | all |
| M-1.2 | Has `name`, `kebab`, `scope: ['soma', ...]` declared | error | all |
| M-1.3 | Has `translations.label` with at least `es` + `en` | error | all |
| M-1.3 | Has `texts.label` as a valid idlangref (`#?components.{kebab}.label\|Fallback`), catalog entry in `langs/components/{kebab}.ts` | error | all |
| M-1.4 | If interactive: `apg` URL declared pointing at the relevant W3C ARIA pattern | warn | interactive |
### A2 · Parts

@ -59,9 +59,9 @@ UIX pone nombres y contratos explícitos a esas separaciones.
### `Morfo` (`src/uix/morfo/`)
Contrato estructural cross-layer del componente. Define `parts`, `data-*`,
ARIA, foco, teclado, eventos y, cuando el texto pertenece al contrato,
`translations` del componente. No es prose ni runtime — es la forma canónica
pública.
ARIA, foco, teclado, eventos y, cuando el texto pertenece al contrato, los
slots `texts` del componente (idlangrefs). No es prose ni runtime — es la
forma canónica pública.
Ver: [morfo/README.md](./morfo/README.md)

@ -21,7 +21,8 @@ directamente.
`events` según opciones;
- conserva `portal` como target genérico de portales, para que cada capa lo
adapte a su API (`portalTo`) sin acoplar `active-uix` a esa capa;
- registra las traducciones comunes y las `morfo.translations`.
- registra las traducciones comunes y los catálogos por componente de
`src/uix/langs/components/*`.
`attachActiveUix(app, options)` — **attach** a un `ActiveApp` externo:
@ -30,7 +31,7 @@ directamente.
- **exige** `app.langs` y `app.dom`; si faltan, lanza error de configuración;
- no vuelve a suscribir `langs` a `prefs.language` (esa conexión pertenece a
`defineActiveLangs` dentro de `ActiveApp`);
- registra las traducciones comunes y las `morfo.translations`;
- registra las traducciones comunes y los catálogos por componente;
- no posee el lifecycle del `app`.
## Contratos mínimos por módulo

@ -68,9 +68,9 @@ export interface ActiveUixOptions {
readonly logger?: LoggerOptions;
/**
* If `true` (default), UIX common defaults are merged and
* `ActiveUix` connects the morfo registry to
* `ActiveLangs` so `morfo.translations` registered later become available.
* If `true` (default), UIX common defaults are merged and `ActiveUix`
* registers the per-component catalogs (`src/uix/langs/components/*`)
* under `components.{kebab}.*`.
*/
readonly registerDefaultLangs?: boolean;

@ -4,7 +4,7 @@
Morfo is the single source of truth for a component's parts, data-attrs, ARIA contract, keyboard shortcuts, focus policy, and public event contract. The same morfo is consumed by soma (to wire the headless provider), by eidos (to generate CSS selectors), by sema (to bind perceptual channels), and by the docs site (to render part tables).
**One file per component**, at `src/uix/morfo/components/{kebab}.ts`. No prose — that's the component's README. No props — those live in `types.ts` with JSDoc. Component-owned translations may live in `translations` when they are part of ARIA labels, live-region text, or internal functional labels. Everything else is the machine-readable contract.
**One file per component**, at `src/uix/morfo/components/{kebab}.ts`. No prose — that's the component's README. No props — those live in `types.ts` with JSDoc. Component-owned text slots may live in `texts` (idlangrefs) when they are part of ARIA labels, live-region text, or internal functional labels — the multilingual catalog itself lives in `src/uix/langs/components/{kebab}.ts`. Everything else is the machine-readable contract.
## Why morfo exists
@ -34,7 +34,7 @@ A `Morfo` is a plain TypeScript constant that describes:
- **`apg`** — optional URL to the WAI-ARIA APG pattern when the component implements a formal one.
- **`focus`** — optional focus policy for overlays / composites.
- **`events`** — the component's public event surface: which semantic occurrences it may emit and expose to cross-layer consumers.
- **`translations`** — optional component-owned translation catalog. Registered under `components.{kebab}` by the morfo registry.
- **`texts`** — optional component-owned text slots, declared as idlangrefs (`'#?components.{kebab}.{key}|Fallback'`). The multilingual catalog itself lives in `src/uix/langs/components/{kebab}.ts` and is registered by `ActiveUix` under `components.{kebab}.*`.
- **`parts`** — the part tree (recursive). Each part declares:
- `name`, `kebab`, `kind` (`public` / `virtual`).
- `archetype` — optional cross-component classification (see "Archetypes" below).
@ -57,7 +57,7 @@ See [`types.ts`](./types.ts) for the full TypeScript shape.
| Usage examples | `{component}/README.md` | Narrative |
| Props (names, types, defaults) | `{component}/types.ts` with JSDoc | Canonical source is TS + JSDoc |
| Shared/common translations | app/langs catalog under `common.*` | Shared vocabulary should not be duplicated per component |
| Provider-only id constants | optional `{component}/langs.ts` | Constants are code ergonomics; the catalog lives in morfo or app langs |
| Provider-only id constants | optional `{component}/langs.ts` | Constants are code ergonomics; the catalog lives in `langs/components/{kebab}.ts` or app langs |
| Event handlers / runtime wiring, state machines | `{component}-provider.svelte.ts` | Execution logic, not contract data |
| Visual variants / recipes | `src/uix/eidos/` (future) | Layer-specific, not shared |
@ -278,10 +278,8 @@ export const dialogMorfo = {
restore: true
},
translations: {
content: {
roledescription: { es: 'ventana de dialogo', en: 'dialog window' }
}
texts: {
'content.roledescription': '#?components.dialog.content.roledescription|dialog window'
},
parts: [
@ -474,21 +472,21 @@ condition: { when: 'prop-truthy', prop: 'modal' }
condition: { when: 'prop-falsy', prop: 'disabled' }
```
### Step 4.5 — Declare component-owned `translations`
### Step 4.5 — Declare component-owned `texts`
If a translation belongs to the component contract, put the catalog on the
morfo:
If a text slot belongs to the component contract, declare it on the morfo as
an idlangref. The morfo never embeds the literal multilingual record — that
lives in `src/uix/langs/components/{kebab}.ts` and is merged into the active
catalog by `ActiveUix`:
```ts
export const dialogMorfo = {
name: 'Dialog',
kebab: 'dialog',
// ...
translations: {
trigger: { es: 'Abrir dialogo', en: 'Open dialog' },
content: {
roledescription: { es: 'ventana de dialogo', en: 'dialog window' }
}
texts: {
trigger: '#?components.dialog.trigger|Open dialog',
'content.roledescription': '#?components.dialog.content.roledescription|dialog window'
},
parts: [
{

@ -76,6 +76,8 @@ const elementSchema = union(
literal('aside'),
literal('footer'),
literal('img'),
literal('video'),
literal('audio'),
literal('svg'),
literal('path'),
literal('g'),
@ -451,7 +453,9 @@ function validateDataAttrName(
* 4. Every `state-equals` condition's `state` exists somewhere in the part.
* 5. `focus.initial.partRef` / `focus.return.partRef` resolve to a part.
* 6. Root `scope` non-empty.
* 7. Component-relative `translationRef` keys resolve inside `morfo.translations`.
* 7. Component-relative `translationRef` keys normalize to
* `#?components.{kebab}.{key}`; catalog presence is checked by
* `scripts/translations-check.ts`, not here.
* 8. Morfo `kebab` is valid kebab-case (a-z0-9-).
*
* Not validated here (requires external sources):

@ -75,6 +75,8 @@ export type MorfoElement =
| 'aside'
| 'footer'
| 'img'
| 'video'
| 'audio'
| 'svg'
| 'path'
| 'g'
@ -788,7 +790,7 @@ export type SemaExpressionMode = 'pack' | 'family-default' | 'delegated' | 'none
* Minimal by design. Editorial content (summary, comparison table,
* when-to-use prose) lives in the component's README, not here.
* Props live in the component's `types.ts` with JSDoc. Component-owned
* translations live in `translations`.
* text slots live in `texts`.
*/
export interface Morfo {
/** Component display name, PascalCase. */

@ -420,7 +420,7 @@ Pattern: `soma-{component}-{part}-{uid}`. Always descriptive.
[ ] 4. Create {name}-provider.svelte.ts with concrete provider/state classes
[ ] 5. Use `soma.runtime()` inside providers (or `createSomaRuntime()` in tests/tools) so `registerMorfo()` runs — A1
[ ] 6. Create types.ts with JSDoc on ALL props + canonical field shapes
[ ] 7. Add `morfo.translations` for component-owned text; create langs.ts only for imperative constants — A3
[ ] 7. Add `morfo.texts` for component-owned text (catalog in `langs/components/{kebab}.ts`); create langs.ts only for imperative constants — A3
[ ] 8. Create wrapper .svelte files (thin: props → Active → Provider → mergeProps → render — A11)
[ ] 9. Verify: event handlers included in props (not just class methods)
[ ] 10. Verify: context captured in constructor, not in handlers
@ -555,7 +555,7 @@ Pattern: `soma-{component}-{part}-{uid}`. Always descriptive.
(or `common.*` for shared strings). Any `langs.t('soma.…')` /
`langs.ts('soma.…')` is a bug and will log `Translation key not
found` at runtime. Component-owned paths should come from
`morfo.translations` + `v.translationRef`; shared paths should use
`morfo.texts` + `v.translationRef`; shared paths should use
`v.commonRef` or an explicit idlangref constant (A3).
[ ] 38. **DOM topology vs `.require()` audit.** For every `X.require()`
@ -595,7 +595,7 @@ Pattern: `soma-{component}-{part}-{uid}`. Always descriptive.
8. **Redeclaring field types** — defining prop types in both `types.ts` and the provider Opts interface. Define canonical shapes once in `types.ts`, reference with `StateProps<>` / `ActiveProps<>`.
9. **Hardcoding aria strings** — declare component-owned text in `morfo.translations` and reference it with `v.translationRef`; use `v.commonRef` / idlangref constants for shared imperative labels. Never inline strings in providers.
9. **Hardcoding aria strings** — declare component-owned text slots in `morfo.texts` and reference them with `v.translationRef`; use `v.commonRef` / idlangref constants for shared imperative labels. Never inline strings in providers.
10. **Gesture capturing child clicks** — `setPointerCapture` must be deferred until moveBuffer is exceeded. Immediate capture on pointerdown steals click events from buttons inside the draggable area.
@ -611,8 +611,8 @@ These rules were extracted from a full audit of all 25 soma components. Every is
### A1. Register the morfo through the runtime
Every component MUST register its morfo before it relies on `assertContract`
or morfo-owned translations. The canonical path is to create a runtime:
Every component MUST register its morfo before it relies on `assertContract`.
The canonical path is to create a runtime:
```ts
const runtime = this.soma.runtime({name}Morfo, { states, props, parts, events });
@ -631,11 +631,11 @@ const runtime = createSomaRuntime({name}Morfo, {
});
```
Both paths call `registerMorfo(morfo)` internally. That compiles the morfo,
registers its `data-*` contract and publishes `morfo.translations` to connected
`ActiveLangs` instances. Only call `registerMorfo(morfo)` manually for a
legacy provider or tool that needs the registry side effect without creating
a runtime.
Both paths call `registerMorfo(morfo)` internally. That compiles the morfo
and registers its `data-*` contract (component text catalogs are registered
separately by `ActiveUix` from `src/uix/langs/components/*`). Only call
`registerMorfo(morfo)` manually for a legacy provider or tool that needs the
registry side effect without creating a runtime.
### A2. Root part must use 'provider', not 'root'
@ -665,14 +665,15 @@ this.runtimePart = this.runtime.part('provider', {
**Soma access:** Declare `readonly soma = Soma.get()` in the root provider when the component needs prefs-derived services or imperative translations. Sub-parts access soma via `this.provider.soma`.
**Translations:** Component-owned text lives in the morfo, not in a per-component catalog file:
**Texts:** The morfo declares component-owned text slots as idlangrefs; the
multilingual catalog lives in `src/uix/langs/components/{kebab}.ts`:
```ts
export const drawerMorfo = {
name: 'Drawer',
kebab: 'drawer',
translations: {
trigger: { es: 'Abrir cajon', en: 'Open drawer' }
texts: {
trigger: '#?components.drawer.trigger|Open drawer'
},
parts: [
{
@ -716,15 +717,15 @@ components.dialog.trigger
- Common keys live under `common.*` at the lang root — not under soma
- Component keys live under `components.{name}.*`
- `ActiveUix` registers `commonLangs` defaults without overwriting user-provided leaves
- `ActiveUix` registers `morfo.translations` dynamically when `registerMorfo(morfo)` runs through the runtime
- There is no global per-component catalog. Component-owned strings live in `morfo.translations`; shared strings live in `common.*`.
- `ActiveUix` registers the per-component catalogs from `src/uix/langs/components/*` under `components.{kebab}.*`
- The morfo only declares slots (`morfo.texts`, idlangrefs); the multilingual records live in `langs/components/{kebab}.ts`. Shared strings live in `common.*`.
- `langs.ts()` with idlangref for simple strings. `langs.t()` only for interpolated templates (e.g., `Page {{value}}`)
Do NOT:
- Create `translate()` helper methods in providers
- Use `?? 'fallback'` — the fallback belongs inside the langref (`#?path|fallback`)
- Hardcode aria strings — use `morfo.translations` + `v.translationRef`, `v.commonRef`, or an explicit idlangref constant
- Hardcode aria strings — use `morfo.texts` + `v.translationRef`, `v.commonRef`, or an explicit idlangref constant
- Put common keys (close, open, cancel) under component namespaces — they belong in `common.*`
**Imports within soma:** Use relative paths, not `$soma/` aliases. Relative paths make the library portable without requiring alias configuration in the consumer's build. soma does not import from `$lib` — trivial utilities (like empty callbacks) are inline (`() => {}`).
@ -1234,7 +1235,7 @@ Three classes of bug cannot be caught by `svelte-check` or HTTP 200 — they all
grep -rn "['\"]soma\.[a-z-]" src --include=!*.md
```
Fixes: route component-owned text through `morfo.translations` + `v.translationRef`; route shared text through `v.commonRef` or an explicit idlangref constant. Don't build translation paths via template strings in demos or providers. If a demo needs a dynamic path helper (like `tt('columns', 'Columns')`), hard-code the namespace prefix `components.{name}.` correctly.
Fixes: route component-owned text through `morfo.texts` + `v.translationRef`; route shared text through `v.commonRef` or an explicit idlangref constant. Don't build translation paths via template strings in demos or providers. If a demo needs a dynamic path helper (like `tt('columns', 'Columns')`), hard-code the namespace prefix `components.{name}.` correctly.
2. **DOM topology vs `.require()` audit.** Svelte's context (via `getContext`) flows only to descendants. Every `X.require()` call must be reachable from a descendant of the component that set the context. The trap is HTML: `<tr>` cannot nest `<tr>`, so `Table.RowDetail` (rendered as a sibling `<tr>` of `Table.Row`) cannot `TableRowProvider.require()`. Use one of:
- Receive the object via a prop (consumer passes `{row}` or similar explicitly). This is consistent with `<Table.Row {row}>` / `<Table.Cell {cell}>` — Table already requires explicit objects.

@ -129,7 +129,7 @@ Cada componente tiene un archivo en `src/uix/morfo/components/{kebab}.ts` que de
- **aria** — qué atributos ARIA emite cada parte, con la fuente del valor tipada vía tagged union (`v.literal`, `v.stateRef`, `v.partRef`, `v.propRef`, `v.translationRef`) y condición de emisión opcional.
- **keyboard** — los atajos de teclado relevantes por parte.
- **focus** — política de foco para overlays (`initial`, `trap`, `return`, `restore`).
- **translations** — catálogo de traducciones propio del componente, registrado bajo `components.{kebab}`.
- **texts** — slots de texto propios del componente, declarados como idlangrefs (`'#?components.{kebab}.{key}|Fallback'`). El catálogo multilingüe vive en `src/uix/langs/components/{kebab}.ts`.
- **apg** — URL al patrón WAI-ARIA APG cuando aplica.
- **scope** — las capas que implementan el componente: `['soma']`, `['soma', 'eidos']`, etc.
@ -140,9 +140,9 @@ vive en [`src/uix/morfo/README.md`](../morfo/README.md).
### Cómo soma consume un morfo
Cada provider raíz crea un runtime con su morfo. Ese paso registra el contrato
`data-*`, compila la declaración y publica `morfo.translations` en los `ActiveLangs`
conectados por `ActiveUix`:
Cada provider raíz crea un runtime con su morfo. Ese paso compila la
declaración y registra el contrato `data-*` (los catálogos de texto por
componente los registra `ActiveUix` desde `src/uix/langs/components/*`):
```ts
import { dialogMorfo } from '../../../morfo/components/dialog';
@ -353,7 +353,7 @@ sema → perception/events: hold, sound, haptic
Eidos consume Soma vía los `data-*` públicos y los subpaths públicos
(`import { Accordion } from '$soma/components/accordion'`); responde a estados
(`[data-accordion][data-state='open'] { ... }`), añade props visuales (`size`,
`variant`, `color`) y reutiliza las translations. Nunca importa Provider classes
`variant`, `color`) y reutiliza los catálogos de texto. Nunca importa Provider classes
internas, no depende de estructura DOM incidental ni duplica behavior que soma
ya resuelve.

@ -145,7 +145,7 @@ del componente (checklist item 27).
soma resuelve ARIA por defecto. El consumidor no necesita añadir `role`, `aria-modal`, `aria-expanded`, `aria-controls`, etc. — el Provider los genera.
Texto funcional se resuelve via `langs.ts()` con idlangref. El catalogo propio del componente vive en `morfo.translations` y se referencia con `v.translationRef(...)`; texto compartido como close/cancel/save vive en `common.*` y se referencia con `v.commonRef(...)` o un idlangref absoluto. `langs.ts` por componente queda como comodidad opcional para constantes imperativas, no como catalogo canonico.
Texto funcional se resuelve via `langs.ts()` con idlangref. El morfo declara sus slots de texto en `morfo.texts` (idlangrefs) y los referencia con `v.translationRef(...)`; el catalogo multilingue vive en `src/uix/langs/components/{kebab}.ts`. Texto compartido como close/cancel/save vive en `common.*` y se referencia con `v.commonRef(...)` o un idlangref absoluto. `langs.ts` por componente queda como comodidad opcional para constantes imperativas, no como catalogo canonico.
### 3.7 DOM global via ActiveDom
@ -685,14 +685,16 @@ standalone functions. No `from()`. No `ctx` exposed.
### Texto funcional
Las traducciones propias del componente se declaran en el morfo:
Los slots de texto propios del componente se declaran en el morfo como
idlangrefs (el catalogo multilingue vive en
`src/uix/langs/components/{kebab}.ts`):
```ts
export const drawerMorfo = {
name: 'Drawer',
kebab: 'drawer',
translations: {
trigger: { es: 'Abrir cajon', en: 'Open drawer' }
texts: {
trigger: '#?components.drawer.trigger|Open drawer'
},
parts: [
{
@ -710,10 +712,11 @@ Las traducciones compartidas no se duplican por componente:
value: v.commonRef('buttons.close', 'Close'); // #?common.buttons.close|Close
```
`ActiveUix` conecta el registro de morfos con `ActiveLangs`. Cuando el
provider crea `createSomaRuntime(morfo, sources)` o `soma.runtime(morfo,
sources)`, `registerMorfo(morfo)` registra el contrato `data-*` y publica
`morfo.translations` bajo `components.{kebab}`.
`ActiveUix` registra los catalogos de `src/uix/langs/components/*` en
`ActiveLangs` bajo `components.{kebab}.*`. Cuando el provider crea
`createSomaRuntime(morfo, sources)` o `soma.runtime(morfo, sources)`,
`registerMorfo(morfo)` compila y registra el contrato `data-*`; el morfo
solo declara sus slots (`morfo.texts`), no el catalogo.
`commonLangs` en `src/uix/langs.ts` aporta los defaults de `common.*`.
`ActiveUix` los registra sin pisar hojas existentes, de modo que el
@ -1028,7 +1031,7 @@ See `COMPONENT_GUIDE.md` for the full step-by-step process (27 general steps + 4
```
[ ] 1. Compare with ark-ui, bits-ui, radix-ui — feature table
[ ] 2. Verify membership criteria
[ ] 3. Define parts + attrs + morfo.translations when the component owns text
[ ] 3. Define parts + attrs + morfo.texts when the component owns text
[ ] 4. Create types.ts (props + canonical field shapes)
[ ] 5. Create langs.ts only for imperative idlangref constants, not as the catalog
[ ] 6. Create {name}-provider.svelte.ts (concrete state classes, no Provider inheritance)

@ -117,7 +117,7 @@ Focus lands on the first focusable child when the dialog opens (`Cancel` by DOM
## i18n
Default button labels resolve through the UIX lang system. The component catalog is owned by `morfo.translations` under `components.alert-dialog`.
Default button labels resolve through the UIX lang system. The morfo declares the slots in `texts`; the catalog lives in `src/uix/langs/components/alert-dialog.ts` under `components.alert-dialog`.
| Key | English | Spanish |
| -------- | --------- | ----------- |

@ -84,7 +84,7 @@ Snippet props: `{ over, accepting }`.
| Droppable | `aria-dropeffect` | `'move'` while accepting, `'none'` otherwise |
| Droppable | `tabindex` | `0` during drag if accepting; `-1` otherwise |
Each drag step is announced via the `Announce` global API. Component-owned announcement strings live in the drag-drop `morfo.translations` catalog. Disable with `announceEnabled={false}` on the Provider when wiring your own.
Each drag step is announced via the `Announce` global API. Component-owned announcement slots are declared in the drag-drop `morfo.texts`; the catalog lives in `src/uix/langs/components/drag-drop.ts`. Disable with `announceEnabled={false}` on the Provider when wiring your own.
## Data Attributes

@ -132,7 +132,7 @@ All keyboard semantics come from the underlying `<input>` — soma does not re-i
## i18n
Default strings live under `components.pin-input`. The canonical catalog is the pin-input morfo `translations`:
Default strings live under `components.pin-input`. The morfo declares the slots in `texts`; the catalog is `src/uix/langs/components/pin-input.ts`:
| Key | English | Spanish |
| ------- | -------------------------------- | -------------------------------- |

@ -71,7 +71,7 @@ The per-endpoint slider / toggle wrappers read their endpoint from the required
## ARIA
- Sliders emit `role="slider"` with per-endpoint `aria-label` resolved through UIX lang refs (`morfo.translations` for component-owned labels, `langs.ts` only for legacy imperative constants).
- Sliders emit `role="slider"` with per-endpoint `aria-label` resolved through UIX lang refs (`morfo.texts` for component-owned labels, `langs.ts` only for legacy imperative constants).
- `DayPeriodToggle` emits `role="radiogroup"` with two `<input type="radio">` items.
- Input / Segment inherit TimeRangeField / TimeField ARIA.

Loading…
Cancel
Save

Powered by TurnKey Linux.