uix(palabras): F-A chunk 2 · switcher de idioma (componente)

palabras-lang-switcher.svelte (eidos): swap del idioma ACTIVO de un documento
multilenguaje. Reflejo del switcher de field-langs: radiogroup segmentado (≤3
idiomas) o Popover con radiogroup (mas), cada opcion con punto de completitud
(langFilled) y teclado de radiogroup (flechas/Home/End). Lee la `api`; el motor
posee el estado.

Api: expone `languageSpecs` (specs crudos con label/dir) para que el switcher
honre etiquetas explicitas; LangCode/LangSpec/PalabrasLangValues re-exportados
del barrel soma. Compila 0 errores.

PENDIENTE (chunk 3): CSS + ubicacion (menubar al final · migas tras Documento) +
demo + verificacion en navegador.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alpha-0.1-sec-dom
dev 3 months ago
parent 090b273911
commit fb90bd1405

@ -0,0 +1,141 @@
<script lang="ts">
/**
* palabras language switcher (F-A) — swaps the ACTIVE content language of a
* multilingual document. Mirrors field-langs' switcher: a segmented radiogroup
* for ≤3 languages, a `Popover`-backed radiogroup for more. Each option carries a
* filled/empty dot (`langFilled`) so authors see which languages still need
* content. Chrome-only: reads the palabras `api`; the engine owns the state.
*/
import { Popover } from '$uix/eidos/components/popover';
import { ActiveEidos } from '$uix/eidos';
import type { ProviderSnippetProps, LangSpec } from '$soma/components/palabras';
let {
api,
variant = 'auto'
}: {
api: ProviderSnippetProps;
/** Force a form, or `'auto'` = segmented for ≤3 languages, select for more. */
variant?: 'auto' | 'segmented' | 'select';
} = $props();
const eidos = ActiveEidos.require();
const label = $derived(
eidos.langs.ts('#?components.palabras.lang.switcher|Idioma del contenido')
);
type Spec = { code: string; label?: string };
const specs = $derived<Spec[]>(
api.languageSpecs.map((s: LangSpec) =>
typeof s === 'string' ? { code: s } : { code: s.code, label: s.label }
)
);
const kind = $derived(variant === 'auto' ? (specs.length <= 3 ? 'segmented' : 'select') : variant);
/** Localized language name (autonym) — accessible name / tooltip / dropdown row. */
function fullName(spec: Spec): string {
if (spec.label) return spec.label;
try {
return new Intl.DisplayNames([spec.code], { type: 'language' }).of(spec.code) ?? spec.code;
} catch {
return spec.code;
}
}
/** Compact visible label — the explicit label, else the uppercased code. */
const shortLabel = (spec: Spec) => spec.label ?? spec.code.toUpperCase();
const activeSpec = $derived(specs.find((s) => s.code === api.activeLang));
let selectOpen = $state(false);
let menuEl = $state<HTMLElement | null>(null);
/** Radiogroup keyboard: arrows / Home / End move + select the active language. */
function onKeydown(e: KeyboardEvent) {
const group = e.currentTarget as HTMLElement;
const codes = api.languages;
const i = codes.indexOf(api.activeLang);
if (i < 0) return;
const rtl = getComputedStyle(group).direction === 'rtl';
const nextKey = rtl ? 'ArrowLeft' : 'ArrowRight';
const prevKey = rtl ? 'ArrowRight' : 'ArrowLeft';
let target = -1;
if (e.key === nextKey || e.key === 'ArrowDown') target = (i + 1) % codes.length;
else if (e.key === prevKey || e.key === 'ArrowUp') target = (i - 1 + codes.length) % codes.length;
else if (e.key === 'Home') target = 0;
else if (e.key === 'End') target = codes.length - 1;
else return;
e.preventDefault();
const code = codes[target];
group.querySelector<HTMLElement>(`[data-lang="${CSS.escape(code)}"]`)?.focus();
api.setActiveLang(code);
}
// Focus the active language when the dropdown opens (so arrow keys work).
$effect(() => {
if (selectOpen && menuEl) menuEl.querySelector<HTMLElement>('[data-active]')?.focus();
});
</script>
{#snippet langRadio(spec: Spec, long: boolean)}
{@const active = spec.code === api.activeLang}
<button
type="button"
role="radio"
aria-checked={active}
tabindex={active ? 0 : -1}
title={fullName(spec)}
aria-label={fullName(spec)}
data-palabras-lang-option
data-lang={spec.code}
data-active={active ? '' : undefined}
data-complete={api.langFilled(spec.code) ? '' : undefined}
onclick={() => {
api.setActiveLang(spec.code);
selectOpen = false;
}}
>
{long ? fullName(spec) : shortLabel(spec)}
</button>
{/snippet}
{#if api.multilingual}
{#if kind === 'segmented'}
<!-- svelte-ignore a11y_interactive_supports_focus -->
<!-- radiogroup uses roving tabindex on its radio children (APG); the group is not a tab stop -->
<div
data-palabras-lang-switcher
data-mode="segmented"
role="radiogroup"
aria-label={label}
onkeydown={onKeydown}
>
{#each specs as spec (spec.code)}
{@render langRadio(spec, false)}
{/each}
</div>
{:else}
<div data-palabras-lang-switcher data-mode="select" aria-label={label}>
<Popover bind:open={selectOpen}>
<Popover.Trigger data-palabras-lang-trigger aria-label={label}>
{activeSpec ? shortLabel(activeSpec) : ''}
<span aria-hidden="true">▾</span>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content data-palabras-lang-menu-content data-palabras-external-tool>
<!-- svelte-ignore a11y_interactive_supports_focus -->
<div
bind:this={menuEl}
data-palabras-lang-menu
role="radiogroup"
aria-label={label}
onkeydown={onKeydown}
>
{#each specs as spec (spec.code)}
{@render langRadio(spec, true)}
{/each}
</div>
</Popover.Content>
</Popover.Portal>
</Popover>
</div>
{/if}
{/if}

@ -45,7 +45,10 @@ export type {
PalabrasLinkEditorSnippetProps as LinkEditorSnippetProps,
PalabrasFindReplaceSnippetProps as FindReplaceSnippetProps,
PalabrasOnUploadImage,
PalabrasImageUploadResult
PalabrasImageUploadResult,
LangCode,
LangSpec,
PalabrasLangValues
} from './types';
export type {

@ -1722,6 +1722,7 @@ export class PalabrasProvider {
readonly snippetProps = $derived.by(() => ({
multilingual: this.multilingual,
languages: this.resolvedLanguages,
languageSpecs: this.opts.languages.current ?? [],
activeLang: this.activeLang,
setActiveLang: (code: LangCode) => this.setActiveLang(code),
missingRequiredLangs: this.missingRequiredLangs,

@ -137,6 +137,8 @@ export type PalabrasProviderSnippetProps = {
readonly multilingual: boolean;
/** Resolved supported language codes (first = default active). */
readonly languages: readonly LangCode[];
/** The raw language specs (explicit labels / dir preserved) — for the switcher. */
readonly languageSpecs: readonly LangSpec[];
/** The language whose document is currently being edited. */
readonly activeLang: LangCode;
/** Switch the active editing language (persists the current one). */

Loading…
Cancel
Save

Powered by TurnKey Linux.