uix(palabras): F-A · doc.languages = fuente de verdad de multilenguaje (cableado)

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 <noreply@anthropic.com>
alpha-0.1-sec-dom
dev 3 months ago
parent 9223956a90
commit 7760a9ce8b

@ -212,6 +212,7 @@ export function foldPalabrasLanguages(
};
return {
...base,
languages: [primary, ...others],
activeLang: primary,
children: base.children.map((block, i) =>
mapLeavesZip(

@ -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',

@ -209,9 +209,14 @@ export class PalabrasProvider {
// the tree (`switchDocumentLanguage`). `doc.activeLang` records the current one.
private activeLangState = $state<LangCode>('');
readonly resolvedLanguages = $derived.by<readonly LangCode[]>(() =>
(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<readonly LangCode[]>(() => {
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,

Loading…
Cancel
Save

Powered by TurnKey Linux.