diff --git a/src/uix/COMPONENT_COMPLETION_CHECKLIST.md b/src/uix/COMPONENT_COMPLETION_CHECKLIST.md index 9af330f4e..664871124 100644 --- a/src/uix/COMPONENT_COMPLETION_CHECKLIST.md +++ b/src/uix/COMPONENT_COMPLETION_CHECKLIST.md @@ -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 diff --git a/src/uix/README.md b/src/uix/README.md index 95dacda5f..f2772bb83 100644 --- a/src/uix/README.md +++ b/src/uix/README.md @@ -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) diff --git a/src/uix/active-uix/README.md b/src/uix/active-uix/README.md index 125fcbb92..b0e443370 100644 --- a/src/uix/active-uix/README.md +++ b/src/uix/active-uix/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 diff --git a/src/uix/active-uix/types.ts b/src/uix/active-uix/types.ts index dce791bb8..627c2107c 100644 --- a/src/uix/active-uix/types.ts +++ b/src/uix/active-uix/types.ts @@ -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; diff --git a/src/uix/morfo/README.md b/src/uix/morfo/README.md index 80ede3360..9e88f32c6 100644 --- a/src/uix/morfo/README.md +++ b/src/uix/morfo/README.md @@ -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: [ { diff --git a/src/uix/morfo/schema.ts b/src/uix/morfo/schema.ts index 2988e13e2..e3b9de688 100644 --- a/src/uix/morfo/schema.ts +++ b/src/uix/morfo/schema.ts @@ -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): diff --git a/src/uix/morfo/types.ts b/src/uix/morfo/types.ts index 47c2e0d56..5296c9b70 100644 --- a/src/uix/morfo/types.ts +++ b/src/uix/morfo/types.ts @@ -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. */ diff --git a/src/uix/soma/COMPONENT_GUIDE.md b/src/uix/soma/COMPONENT_GUIDE.md index 13dec6abe..b004c664b 100644 --- a/src/uix/soma/COMPONENT_GUIDE.md +++ b/src/uix/soma/COMPONENT_GUIDE.md @@ -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: `