From 7760a9ce8b6e9d393b323a2132892268dfb0c8c3 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 21 Jul 2026 14:14:38 +0200 Subject: [PATCH] =?UTF-8?q?uix(palabras):=20F-A=20=C2=B7=20doc.languages?= =?UTF-8?q?=20=3D=20fuente=20de=20verdad=20de=20multilenguaje=20(cableado)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El rework Camino A (461be12d9, otra sesion) dejaba `multilingual` derivado de la PROP `languages`; el schema `doc.languages` (9223956a9) estaba puesto pero sin cablear. Ahora el DOCUMENTO se auto-describe: - resolvedLanguages: `doc.languages` (si presente) manda; la prop = seed/override. - constructor: deriva de `initialDoc.languages ?? prop`; SELLA `doc.languages` en el doc cuando se siembra desde la prop (asi se auto-describe en adelante). - foldPalabrasLanguages ahora sella `doc.languages` (predataba el campo). - languageSpecs (etiquetas del switcher) reconciliado con resolvedLanguages. 2 tests nuevos (doc.languages SIN prop → multilingual; prop-seeded → sella doc.languages). 62/62 provider · 502/502 motor · 0 errores. Verificado Playwright: misma estructura en es/en (compartida por construccion), contenido por idioma. Co-Authored-By: Claude Opus 4.8 --- .../soma/components/palabras/engine/i18n.ts | 1 + .../palabras/palabras-provider.svelte.test.ts | 37 +++++++++++++++++++ .../palabras/palabras-provider.svelte.ts | 28 +++++++++++--- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/uix/soma/components/palabras/engine/i18n.ts b/src/uix/soma/components/palabras/engine/i18n.ts index 262f8ebf2..2005ea5bb 100644 --- a/src/uix/soma/components/palabras/engine/i18n.ts +++ b/src/uix/soma/components/palabras/engine/i18n.ts @@ -212,6 +212,7 @@ export function foldPalabrasLanguages( }; return { ...base, + languages: [primary, ...others], activeLang: primary, children: base.children.map((block, i) => mapLeavesZip( diff --git a/src/uix/soma/components/palabras/palabras-provider.svelte.test.ts b/src/uix/soma/components/palabras/palabras-provider.svelte.test.ts index 4e778de5a..c7e882dba 100644 --- a/src/uix/soma/components/palabras/palabras-provider.svelte.test.ts +++ b/src/uix/soma/components/palabras/palabras-provider.svelte.test.ts @@ -2704,6 +2704,43 @@ describe('PalabrasProvider · multilingual (F-A)', () => { dom.dispose(); }); + it('doc.languages is the SOURCE OF TRUTH — multilingual WITHOUT the languages prop', () => { + const { dom } = installSomaHarness(); + const root = document.createElement('div'); + document.body.append(root); + // The folded doc carries `doc.languages`; the `languages` PROP is left unset. + const opts = palabrasOpts(root, foldPalabrasLanguages({ es: doc('Hola'), en: doc('Hello') }, 'es')); + const { result, cleanup } = withEffectRoot(() => { + const provider = PalabrasProvider.create(opts); + vi.spyOn(PalabrasProvider, 'require').mockReturnValue(provider); + return { provider }; + }); + expect(result.provider.multilingual).toBe(true); // from doc.languages, not a prop + expect(result.provider.resolvedLanguages).toEqual(['es', 'en']); + result.provider.setActiveLang('en'); + expect(result.provider.plainText).toBe('Hello'); + cleanup(); + dom.dispose(); + }); + + it('a prop-seeded multilingual doc self-describes (stamps doc.languages)', () => { + const { dom } = installSomaHarness(); + const root = document.createElement('div'); + document.body.append(root); + const opts = palabrasOpts(root, doc('Hola')); // MONO doc, no doc.languages + opts.languages.current = ['es', 'en']; // the prop SEEDS the language set + const { result, cleanup } = withEffectRoot(() => { + const provider = PalabrasProvider.create(opts); + vi.spyOn(PalabrasProvider, 'require').mockReturnValue(provider); + return { provider }; + }); + expect(result.provider.multilingual).toBe(true); + // The doc got STAMPED with `languages`, so it self-describes going forward. + expect((opts.value.current as { languages?: string[] }).languages).toEqual(['es', 'en']); + cleanup(); + dom.dispose(); + }); + it('setActiveLang swaps the active content; ONE doc carries both languages', () => { const { provider, opts, cleanup, dom } = multiProvider({ es: doc('Hola'), en: doc('Hello') }, [ 'es', diff --git a/src/uix/soma/components/palabras/palabras-provider.svelte.ts b/src/uix/soma/components/palabras/palabras-provider.svelte.ts index 80324f762..3eb8f7d2a 100644 --- a/src/uix/soma/components/palabras/palabras-provider.svelte.ts +++ b/src/uix/soma/components/palabras/palabras-provider.svelte.ts @@ -209,9 +209,14 @@ export class PalabrasProvider { // the tree (`switchDocumentLanguage`). `doc.activeLang` records the current one. private activeLangState = $state(''); - readonly resolvedLanguages = $derived.by(() => - (this.opts.languages.current ?? []).map(langCodeOf) - ); + // `doc.languages` (the document's own list) is the SOURCE OF TRUTH for whether a + // document is multilingual — it self-describes. The `languages` prop is only a + // seed / override for a document that lacks the field. + readonly resolvedLanguages = $derived.by(() => { + const fromDoc = this.document.languages; + if (fromDoc && fromDoc.length > 0) return fromDoc; + return (this.opts.languages.current ?? []).map(langCodeOf); + }); get multilingual(): boolean { return this.resolvedLanguages.length > 0; } @@ -347,11 +352,17 @@ export class PalabrasProvider { // here (and writing the id-stamped doc back to `value`) is the single // point that guarantees ids exist before the first paint. let initialDoc = normalizeDocument(opts.value.current).document; - if (this.multilingual) { + // `doc.languages` is the source of truth; the `languages` prop seeds a doc that + // lacks it. `this.document` isn't set yet, so derive from `initialDoc` here. + const initLangs = + initialDoc.languages && initialDoc.languages.length > 0 + ? initialDoc.languages + : (opts.languages.current ?? []).map(langCodeOf); + if (initLangs.length > 0) { // One document carries every language (`block.i18n`); `doc.activeLang` // records which one lives in `children`. Reconcile the requested active // language with the doc's, swapping content into place if they differ. - const langs = this.resolvedLanguages; + const langs = initLangs; const childrenLang = initialDoc.activeLang ?? langs[0]; const requested = opts.activeLang.current; const desired = @@ -364,6 +375,9 @@ export class PalabrasProvider { initialDoc = switchDocumentLanguage(initialDoc, childrenLang, desired); else if (initialDoc.activeLang !== desired) initialDoc = { ...initialDoc, activeLang: desired }; + // Stamp `languages` so a prop-seeded document self-describes from here on. + if (!initialDoc.languages || initialDoc.languages.length === 0) + initialDoc = { ...initialDoc, languages: langs }; this.activeLangState = desired; this.opts.activeLang.current = desired; } @@ -1701,7 +1715,9 @@ export class PalabrasProvider { readonly snippetProps = $derived.by(() => ({ multilingual: this.multilingual, languages: this.resolvedLanguages, - languageSpecs: this.opts.languages.current ?? [], + languageSpecs: this.resolvedLanguages.map( + (code) => (this.opts.languages.current ?? []).find((s) => langCodeOf(s) === code) ?? code + ), activeLang: this.activeLang, setActiveLang: (code: LangCode) => this.setActiveLang(code), missingRequiredLangs: this.missingRequiredLangs,