docs(plan): v1.0 cycle — fases 17–20 (versioning + list flow + emojis + euskera)
Four planned-fase docs for the first tagged release.
- Fase 17 — semver git tags + CHANGELOG.md (Keep-a-Changelog, [new]/
[fix]/[tweak] prefixes), one big v0.0.0-beta backfill block covering
Fases 0–16, About-modal in /settings rendering bundled CHANGELOG via
?raw import, deploy-script tag check + tag column in .deploys.log.
- Fase 18 — shopping list flow: required title (client + DB tighten),
per-collective per-prefix #N auto-numbering parsed from typed title,
collectives.default_list_title, collective_list_titles catalog + admin
RPCs, autocomplete union (catalog + last-10). Migration 027.
- Fase 19 — EMOJI_CATALOG with faces/food/animals/objects categories
(~280 codepoints, Unicode 14 baseline for iOS<17 compat), new
EmojiPicker.svelte replacing the 8-emoji hardcoded selector at 4
sites. No migration.
- Fase 20 — Euskera locale: messages/eu.json via machine-translate
seed flagged with [needs review], users.language check constraint
extended to ('en','es','eu'), accept-language parser + selector in
/settings. Migration 028. v1.0.0 tag lands at the end of this fase.
README + CLAUDE.md updated with the v1.0 cycle pointers.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
**Fase 16: loading spinner** — `Spinner.svelte` (sizes sm/md/lg = 16/24/40px, `currentColor` for theme, `motion-reduce:animate-none`, `role="status"` + sr-only `m.loading()`) integrated at 7 loading sites (auth splash + search + notes archive + create-collective modal + manage page: members-list / invite generator / add-common-item button). No DB migration; reuses existing `m.loading()`. Added 5 Vitest unit (SP-U-01..05) + 3 Playwright e2e (SP-E-01..03). Vitest config gained `resolve.conditions: ['browser']` + `server.deps.inline: [/^svelte/]` so Svelte 5's `mount()` resolves the browser entry; see `docs/history/fase-16-spinner.md`. **Final cumulative MVP2 (fases 9–16): 177 pgTAP + 179 Vitest integration + 52 Vitest unit + 96 Playwright + 1 gated rate-limit = 505 tests green; 3 skipped.**
|
**Fase 16: loading spinner** — `Spinner.svelte` (sizes sm/md/lg = 16/24/40px, `currentColor` for theme, `motion-reduce:animate-none`, `role="status"` + sr-only `m.loading()`) integrated at 7 loading sites (auth splash + search + notes archive + create-collective modal + manage page: members-list / invite generator / add-common-item button). No DB migration; reuses existing `m.loading()`. Added 5 Vitest unit (SP-U-01..05) + 3 Playwright e2e (SP-E-01..03). Vitest config gained `resolve.conditions: ['browser']` + `server.deps.inline: [/^svelte/]` so Svelte 5's `mount()` resolves the browser entry; see `docs/history/fase-16-spinner.md`. **Final cumulative MVP2 (fases 9–16): 177 pgTAP + 179 Vitest integration + 52 Vitest unit + 96 Playwright + 1 gated rate-limit = 505 tests green; 3 skipped.**
|
||||||
|
|
||||||
|
**v1.0 — planned (📋):** four fases on `mvp2` branch (will retag at the end). Aimed at producing the first tagged release: **Fase 17 versioning** — git tags (`vX.Y.Z` semver), `CHANGELOG.md` in repo root (Keep-a-Changelog format, `[new]/[fix]/[tweak]` prefixes), one big `[v0.0.0-beta]` backfill section covering Fases 0–16, About-modal in `/settings` rendering bundled CHANGELOG via `?raw` import, deploy-script tag check + tag column in `.deploys.log` (`plan/fase-17-versioning-changelog.md`) | **Fase 18 shopping list flow** — required title (client + DB tightening), `#N` auto-numbering parsed from prefix per collective, `collectives.default_list_title` + `collective_list_titles` catalog with admin-only RPCs, autocomplete union catalog + last-10 titles (`plan/fase-18-shopping-list-flow.md`, migration 027) | **Fase 19 emoji avatars** — `EMOJI_CATALOG` lazy-load per category (faces / food / animals / objects, ~280 codepoints, Unicode 14 baseline for iOS<17 compat), new `EmojiPicker.svelte` replacing the 8-emoji hardcoded selector at 4 sites (`plan/fase-19-emoji-avatars.md`, no migration) | **Fase 20 Euskera locale** — `messages/eu.json` via machine-translate seed + `[needs review]` marker, `users.language` check constraint extended to `('en','es','eu')`, `accept-language` parser + selector in `/settings`; v1.0.0 tag lands at the end of this fase (`plan/fase-20-euskera-locale.md`, migration 028).
|
||||||
|
|
||||||
## Documentation Map
|
## Documentation Map
|
||||||
|
|
||||||
- `README.md` — full development plan, confirmed tech stack, Justfile reference, technical warnings
|
- `README.md` — full development plan, confirmed tech stack, Justfile reference, technical warnings
|
||||||
|
|||||||
@@ -28,6 +28,11 @@
|
|||||||
| ↳ Fase 14 — PWA hardening | ✅ |
|
| ↳ Fase 14 — PWA hardening | ✅ |
|
||||||
| ↳ Fase 15 — Common items management | ✅ |
|
| ↳ Fase 15 — Common items management | ✅ |
|
||||||
| ↳ Fase 16 — Loading spinner | ✅ |
|
| ↳ Fase 16 — Loading spinner | ✅ |
|
||||||
|
| **v1.0** — Fase 17 → 20 | 📋 Planificada |
|
||||||
|
| ↳ Fase 17 — Versioning + CHANGELOG | 📋 |
|
||||||
|
| ↳ Fase 18 — Shopping list flow | 📋 |
|
||||||
|
| ↳ Fase 19 — Emoji avatars | 📋 |
|
||||||
|
| ↳ Fase 20 — Euskera locale | 📋 (cierra `v1.0.0`) |
|
||||||
| Suite de tests (pgTAP + Vitest + Playwright) | ✅ **505 tests verdes** (177 pgTAP + 179 integración + 52 unit + 96 E2E) + 1 rate-limit gated (`just test-rate-limit`). 3 skipped en `just test-all` (2 Realtime presence — upstream `handle_out/3` bug; 1 rate-limit — gated). |
|
| Suite de tests (pgTAP + Vitest + Playwright) | ✅ **505 tests verdes** (177 pgTAP + 179 integración + 52 unit + 96 E2E) + 1 rate-limit gated (`just test-rate-limit`). 3 skipped en `just test-all` (2 Realtime presence — upstream `handle_out/3` bug; 1 rate-limit — gated). |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -63,6 +68,10 @@ Las fases ya cerradas (0, 1, 2a) reflejan este patrón retroactivamente: la secc
|
|||||||
| **MVP2 — Fase 14** | [fase-14-pwa-hardening.md](plan/fase-14-pwa-hardening.md) | 3–5 días |
|
| **MVP2 — Fase 14** | [fase-14-pwa-hardening.md](plan/fase-14-pwa-hardening.md) | 3–5 días |
|
||||||
| **MVP2 — Fase 15** | [fase-15-common-items.md](plan/fase-15-common-items.md) | 3–5 días |
|
| **MVP2 — Fase 15** | [fase-15-common-items.md](plan/fase-15-common-items.md) | 3–5 días |
|
||||||
| **MVP2 — Fase 16** | [fase-16-spinner.md](plan/fase-16-spinner.md) | 1 día |
|
| **MVP2 — Fase 16** | [fase-16-spinner.md](plan/fase-16-spinner.md) | 1 día |
|
||||||
|
| **v1.0 — Fase 17** | [fase-17-versioning-changelog.md](plan/fase-17-versioning-changelog.md) | 2–3 días |
|
||||||
|
| **v1.0 — Fase 18** | [fase-18-shopping-list-flow.md](plan/fase-18-shopping-list-flow.md) | 3–5 días |
|
||||||
|
| **v1.0 — Fase 19** | [fase-19-emoji-avatars.md](plan/fase-19-emoji-avatars.md) | 1–2 días |
|
||||||
|
| **v1.0 — Fase 20** | [fase-20-euskera-locale.md](plan/fase-20-euskera-locale.md) | 1–2 días (+ review humana) |
|
||||||
|
|
||||||
**Total estimado MVP (Fase 0–6): 11–16 semanas. MVP2 (Fase 9–14, rama `mvp2`): 10–14 semanas. Fase 7+8 entre MVP y MVP2 como cleanup post-launch.**
|
**Total estimado MVP (Fase 0–6): 11–16 semanas. MVP2 (Fase 9–14, rama `mvp2`): 10–14 semanas. Fase 7+8 entre MVP y MVP2 como cleanup post-launch.**
|
||||||
|
|
||||||
|
|||||||
126
plan/fase-17-versioning-changelog.md
Normal file
126
plan/fase-17-versioning-changelog.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
### Fase 17 — Versioning, git tags + CHANGELOG + about modal
|
||||||
|
|
||||||
|
**v1.0 · 1/4.** First fase of the v1.0 cycle (post-MVP2). Lands the version-tracking infrastructure that every later v1.0 fase will register entries against.
|
||||||
|
|
||||||
|
**Estado: 📋 Planificada.** Atiende cuatro asks acoplados:
|
||||||
|
|
||||||
|
1. **Git tags** — usar `vMAJOR.MINOR.PATCH` semver. Tag creado localmente; el script de deploy verifica que `HEAD` apunta a un tag antes de empezar (warn pero no bloquea).
|
||||||
|
2. **CHANGELOG.md** — formato Keep-a-Changelog. Cada release una sección `## [vX.Y.Z] — YYYY-MM-DD` con sub-bloques `### Added / Changed / Fixed`. Cada bullet anotado `[new]`, `[fix]` o `[tweak]`. Backfill de toda la historia previa como una sola sección beta.
|
||||||
|
3. **About modal en `/settings`** — abre desde la sección "Acerca de" (ya existe el chip de versión, Fase 14.3.4); el modal renderiza `CHANGELOG.md` completo, importado en build-time (`?raw`) — sin fetch en runtime.
|
||||||
|
4. **v1.0.0** se etiqueta al cierre de Fase 20 (Euskera), no aquí.
|
||||||
|
|
||||||
|
**Decisiones clave:**
|
||||||
|
- Versión canónica vive en el `package.json` de la raíz (lo usa Fase 14.3 via `__APP_VERSION__`). Bump manual antes de tag.
|
||||||
|
- `CHANGELOG.md` en raíz, no en `docs/`. Tooling como GitHub releases lo busca ahí por convención.
|
||||||
|
- Backfill BETA = **una sola sección** `## [v0.0.0-beta] — 2026-05-19 — MVP + MVP2 (Fases 0–16)` con 30–50 bullets organizados por área (auth, lists, tasks, notes, PWA, infra). Sin sub-secciones por fase — los plan/history docs ya cubren ese nivel.
|
||||||
|
- Modal de changelog: parser markdown ligero (no librería; basta `marked` si ya está en el bundle, si no `String.replace` para `###`/`-`/`**`). Solo renderiza secciones — no enlaces externos.
|
||||||
|
- Tag creation: manual `git tag -a vX.Y.Z -m "..."` por ahora. No automatizar push de tags hasta que el flujo esté probado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 17.1 CHANGELOG.md backfill
|
||||||
|
|
||||||
|
**Fichero:** nuevo `CHANGELOG.md` en raíz.
|
||||||
|
|
||||||
|
**Tareas:**
|
||||||
|
|
||||||
|
- [ ] **17.1.1** Crear `CHANGELOG.md` con cabecera estándar Keep-a-Changelog:
|
||||||
|
```markdown
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v0.0.0-beta] — 2026-05-19 — MVP + MVP2 (Fases 0–16)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
- [ ] **17.1.2** Backfill el bloque BETA leyendo cada `docs/history/fase-*.md` + `plan/fase-*.md`. 30–50 bullets agrupados:
|
||||||
|
- **Auth + identity** (Fase 1, 7, 8)
|
||||||
|
- **Shopping lists + items** (Fase 2a, 2b, 4, 5, 7, 10, 11, 15)
|
||||||
|
- **Tasks + notes** (Fase 3, 9.3)
|
||||||
|
- **Search** (Fase 4)
|
||||||
|
- **Mobile UX + accessibility** (Fase 5, 9, 16)
|
||||||
|
- **PWA + offline** (Fase 6, 14)
|
||||||
|
- **Collective management** (Fase 10, 12, 13)
|
||||||
|
- **Server admin** (Fase 13)
|
||||||
|
- **i18n** (Fase 0, 4)
|
||||||
|
- **Infra + deploy** (Fase 0, 6, 8, 14.3, plus the off-stack proxy + Caddy + backup/rollback work)
|
||||||
|
- [ ] **17.1.3** Cada bullet con prefix `[new]` / `[fix]` / `[tweak]`. Ejemplos:
|
||||||
|
- `[new] Item tags scoped per collective + drag-reorder (Fase 11).`
|
||||||
|
- `[fix] auth.users role/aud trigger to keep PostgREST routing past Keycloak provisioning (migrations 013–014).`
|
||||||
|
- `[tweak] Edge proxy off-stack — internal Caddy on :3000 + external TLS terminator.`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 17.2 About modal + changelog viewer
|
||||||
|
|
||||||
|
**Ficheros:** nuevo `apps/web/src/lib/components/ChangelogModal.svelte`, edits a `apps/web/src/routes/(app)/settings/+page.svelte`.
|
||||||
|
|
||||||
|
**Tareas:**
|
||||||
|
|
||||||
|
- [ ] **17.2.1** Importar el CHANGELOG en build-time: añadir `import changelogRaw from '../../../../CHANGELOG.md?raw'` en el módulo del modal. Verificar que el path se resuelve desde `apps/web/` (3 niveles arriba); ajustar si falla bajo el adapter-node.
|
||||||
|
- [ ] **17.2.2** Parser ligero inline: split por líneas, formatear `##`/`###` como headers, `-` como list items, `**text**` como bold via `<strong>`. Sin XSS surface — el input es estático del repo, no usuario.
|
||||||
|
- [ ] **17.2.3** `ChangelogModal.svelte`:
|
||||||
|
- Trigger: prop `open: boolean` + `onClose`.
|
||||||
|
- Layout: overlay + panel centered (reusar el patrón de `CreateCollectiveModal.svelte`).
|
||||||
|
- Header: "Historial de cambios" + close icon.
|
||||||
|
- Body: scrollable, max-h-[70vh]. Sticky version chips a la izquierda (opcional).
|
||||||
|
- Cierra con Esc o click en overlay.
|
||||||
|
- [ ] **17.2.4** En `/settings`, debajo del chip de versión existente, añadir un botón discreto "Ver historial" (text-xs subrayado) que setea `showChangelog = true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 17.3 Deploy script tag check
|
||||||
|
|
||||||
|
**Fichero:** `infra/scripts/deploy-erosi.sh`.
|
||||||
|
|
||||||
|
- [ ] **17.3.1** Al inicio (antes del rsync), si `git describe --exact-match --tags HEAD 2>/dev/null` falla (HEAD no es un tag), imprimir advertencia amarilla:
|
||||||
|
```
|
||||||
|
WARNING: HEAD is not at a tagged commit. Deploys without a tag won't
|
||||||
|
map cleanly to CHANGELOG entries. Continue? [5s abort]
|
||||||
|
```
|
||||||
|
Sleep 5s con Ctrl-C abort. Si está taggeado, mostrar el tag al lado de la SHA en el banner.
|
||||||
|
- [ ] **17.3.2** Cuando esté en un tag, incluir el tag (no solo la SHA) en `.deploys.log` como columna 4 opcional: `<ts>\t<sha>\t<backup>\t<tag-or-empty>`. Mantener compat: las 3 columnas anteriores siguen ahí.
|
||||||
|
- [ ] **17.3.3** `rollback-erosi.sh --list` muestra el tag si está presente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 17.4 Tests
|
||||||
|
|
||||||
|
- [ ] **17.4.1** Vitest unit `apps/web/src/lib/components/ChangelogModal.test.ts`:
|
||||||
|
- CHM-U-01 modal cerrado no renderiza nada.
|
||||||
|
- CHM-U-02 modal abierto renderiza el `<h1>` del CHANGELOG.
|
||||||
|
- CHM-U-03 click en overlay dispara `onClose`.
|
||||||
|
- CHM-U-04 tecla Esc dispara `onClose`.
|
||||||
|
- [ ] **17.4.2** Playwright `tests/e2e/changelog.test.ts`:
|
||||||
|
- CL-01 Ana abre `/settings`, clica "Ver historial", modal aparece con la cabecera "v0.0.0-beta" visible.
|
||||||
|
- CL-02 Esc cierra el modal.
|
||||||
|
- [ ] **17.4.3** Unit linter: chequeo del CHANGELOG con un script `scripts/check-changelog.mjs` que valida (a) cabecera Keep-a-Changelog presente, (b) cada release tiene formato `## [vX.Y.Z] — YYYY-MM-DD`, (c) cada bullet empieza con `[new]`/`[fix]`/`[tweak]`. Wire en `just check`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 17.Z Verificación + cierre
|
||||||
|
|
||||||
|
- [ ] **17.Z.1** `just test-all` verde. Suma esperada: +4 unit + 2 e2e + 0 pgTAP = +6.
|
||||||
|
- [ ] **17.Z.2** Smoke: abrir `/settings` en prod, ver el botón "Ver historial", abrir el modal, verificar contenido.
|
||||||
|
- [ ] **17.Z.3** Bump `package.json` version → `1.0.0-rc.0` para empezar el ciclo v1.0 (el `1.0.0` se reserva para el cierre de Fase 20). Tag `v1.0.0-rc.0` desde HEAD.
|
||||||
|
- [ ] **17.Z.4** Documentar en `docs/history/fase-17-versioning-changelog.md`.
|
||||||
|
- [ ] **17.Z.5** Strings Paraglide nuevos: `settings_about_view_changelog`, `changelog_modal_title`, `changelog_modal_close`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Riesgos / notas
|
||||||
|
|
||||||
|
- **`?raw` import del CHANGELOG**: Vite lo bundlea como string al servir el módulo. Bundle size sube ~5–20KB. Si crece más allá de eso (varias releases), considerar lazy-load del modal vía dynamic `import()`.
|
||||||
|
- **Tag check no bloquea el deploy**: warn + sleep, no abort. Mantener el flujo "deploy en cualquier commit" disponible para hotfixes urgentes.
|
||||||
|
- **Linter de CHANGELOG**: no fail-loud en pre-commit hooks; rompe el flujo si alguien edita el CHANGELOG manualmente. Mejor warn-only.
|
||||||
|
- **Markdown parser inline**: lo suficiente para Keep-a-Changelog (headers, lists, bold). Si el CHANGELOG crece con tablas, code blocks, links → swap a `marked` (que ya está si se añade un plugin de markdown para algún otro flujo) o `micromark-light`.
|
||||||
|
|
||||||
|
### Scope explícito fuera
|
||||||
|
|
||||||
|
- Auto-generación del changelog desde commits (conventional-commits + standard-version). Manual por ahora.
|
||||||
|
- GitHub Releases sync. Solo tags locales.
|
||||||
|
- Push de tags al remoto desde el deploy script. Manual.
|
||||||
|
- Versionado de la API/REST. Solo del cliente.
|
||||||
|
- Notificación push de "nueva versión" — Fase 14.1 ya cubre el toast.
|
||||||
189
plan/fase-18-shopping-list-flow.md
Normal file
189
plan/fase-18-shopping-list-flow.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
### Fase 18 — Shopping list creation flow (required title + numbered prefix + autocomplete + defaults)
|
||||||
|
|
||||||
|
**v1.0 · 2/4.** Segunda fase del ciclo v1.0.
|
||||||
|
|
||||||
|
**Estado: 📋 Planificada.** Cuatro asks acoplados sobre el momento "creo una lista nueva":
|
||||||
|
|
||||||
|
1. **Título requerido** — no permitir crear sin nombre. Cliente bloquea + DB tightens.
|
||||||
|
2. **Auto-numerado `#N`** — si el título escrito coincide con un prefijo usado en el colectivo (ej. `Compra`), sugerir `Compra #N` con `N = max(N) + 1`. El `#N` es **parte del título** (campo `name`), no columna separada.
|
||||||
|
3. **Default list title** — admin del colectivo configura un título por defecto que prefilla el input del modal create-list.
|
||||||
|
4. **Catálogo de títulos** — admin gestiona una tabla de títulos sugeridos (CRUD inline en `/collective/manage`); aparecen en el autocomplete junto a los últimos 10 títulos reales.
|
||||||
|
|
||||||
|
**Decisiones clave:**
|
||||||
|
- `shopping_lists.name` ya es `NOT NULL` (migración 005). Falta enforce mínimo en cliente + check de longitud > 0 con trim.
|
||||||
|
- Nuevo `collectives.default_list_title text` (nullable). Vacío = no prefill.
|
||||||
|
- Nuevo `collective_list_titles (collective_id, title text, primary key)` — mirror del shape de `item_frequency`. Solo admin escribe via RPC.
|
||||||
|
- Autocomplete: union de (a) `collective_list_titles` + (b) últimos 10 `name` distintos de `shopping_lists` (mismo colectivo). Deduplicate case-insensitive.
|
||||||
|
- `#N` parsing: regex `^(.+?)\s*(?:#(\d+))?\s*$`. Prefijo = grupo 1. Si el usuario escribe `Compra`, miramos `name ILIKE 'Compra %' OR name ILIKE 'Compra'` y extraemos el `#N` maximal. Sugerimos `Compra #N+1`. Si el usuario escribe `Compra #3` explícitamente, no auto-bump.
|
||||||
|
- Numbering scope = per-collective per-prefix (case-insensitive prefix match).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 18.1 Modelo
|
||||||
|
|
||||||
|
**Fichero:** `supabase/migrations/027_list_title_catalog.sql`.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Default per-collective title (single string, nullable).
|
||||||
|
alter table public.collectives
|
||||||
|
add column default_list_title text;
|
||||||
|
|
||||||
|
-- Catalog of admin-curated suggested titles.
|
||||||
|
create table public.collective_list_titles (
|
||||||
|
collective_id uuid not null references public.collectives(id) on delete cascade,
|
||||||
|
title text not null,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
primary key (collective_id, title)
|
||||||
|
);
|
||||||
|
alter table public.collective_list_titles enable row level security;
|
||||||
|
|
||||||
|
-- Members read; only RPC writes.
|
||||||
|
create policy "select_member" on public.collective_list_titles
|
||||||
|
for select using (public.is_member(collective_id));
|
||||||
|
create policy "deny_direct_writes" on public.collective_list_titles
|
||||||
|
for all using (false) with check (false);
|
||||||
|
|
||||||
|
-- Admin-only RPCs.
|
||||||
|
create or replace function public.add_list_title(
|
||||||
|
p_collective_id uuid, p_title text
|
||||||
|
) returns void
|
||||||
|
language plpgsql security definer
|
||||||
|
set search_path = public, auth
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
v_role text;
|
||||||
|
v_norm text := trim(p_title);
|
||||||
|
begin
|
||||||
|
if auth.uid() is null then raise exception 'not authenticated' using errcode = 'P0001'; end if;
|
||||||
|
if v_norm = '' then raise exception 'title cannot be empty' using errcode = 'P0001'; end if;
|
||||||
|
select role into v_role from public.collective_members
|
||||||
|
where collective_id = p_collective_id and user_id = auth.uid();
|
||||||
|
if v_role is distinct from 'admin' then
|
||||||
|
raise exception 'only admins can curate list titles' using errcode = 'P0002';
|
||||||
|
end if;
|
||||||
|
insert into public.collective_list_titles (collective_id, title)
|
||||||
|
values (p_collective_id, v_norm)
|
||||||
|
on conflict do nothing;
|
||||||
|
end; $$;
|
||||||
|
|
||||||
|
create or replace function public.remove_list_title(
|
||||||
|
p_collective_id uuid, p_title text
|
||||||
|
) returns void
|
||||||
|
language plpgsql security definer
|
||||||
|
set search_path = public, auth
|
||||||
|
as $$
|
||||||
|
declare v_role text;
|
||||||
|
begin
|
||||||
|
if auth.uid() is null then raise exception 'not authenticated' using errcode = 'P0001'; end if;
|
||||||
|
select role into v_role from public.collective_members
|
||||||
|
where collective_id = p_collective_id and user_id = auth.uid();
|
||||||
|
if v_role is distinct from 'admin' then
|
||||||
|
raise exception 'only admins can curate list titles' using errcode = 'P0002';
|
||||||
|
end if;
|
||||||
|
delete from public.collective_list_titles
|
||||||
|
where collective_id = p_collective_id and title = trim(p_title);
|
||||||
|
end; $$;
|
||||||
|
|
||||||
|
grant execute on function public.add_list_title(uuid, text) to authenticated;
|
||||||
|
grant execute on function public.remove_list_title(uuid, text) to authenticated;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tareas:**
|
||||||
|
|
||||||
|
- [ ] **18.1.1** Migración 027.
|
||||||
|
- [ ] **18.1.2** Tests pgTAP `supabase/tests/019_list_title_catalog.sql`:
|
||||||
|
- default_list_title default NULL, admin update OK, member update denied via RLS.
|
||||||
|
- add_list_title como admin OK, idempotente por unique.
|
||||||
|
- add_list_title como member → P0002.
|
||||||
|
- add_list_title con título vacío → P0001.
|
||||||
|
- remove_list_title como admin borra fila.
|
||||||
|
- remove_list_title como guest → P0002.
|
||||||
|
- [ ] **18.1.3** `just db-types`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 18.2 Stores
|
||||||
|
|
||||||
|
**Fichero:** nuevo `apps/web/src/lib/stores/listTitles.ts`, extender `apps/web/src/lib/stores/lists.ts`.
|
||||||
|
|
||||||
|
- [ ] **18.2.1** Store `listTitles`:
|
||||||
|
- `loadTitleCatalog(collectiveId)` — select * from `collective_list_titles`.
|
||||||
|
- `addTitle(collectiveId, title)` / `removeTitle(collectiveId, title)` — RPCs.
|
||||||
|
- [ ] **18.2.2** Helper `fetchTitleSuggestions(collectiveId, prefix)` — union catalog + últimos 10 `shopping_lists.name` distintos. Dedupe case-insensitive. Limit 15.
|
||||||
|
- [ ] **18.2.3** Helper `computeNextNumber(collectiveId, basePrefix)`:
|
||||||
|
- Query: `select name from shopping_lists where collective_id = $1 and name ILIKE $2 and deleted_at is null`.
|
||||||
|
- Pattern `$2 = basePrefix || ' #%'` plus `basePrefix` exact.
|
||||||
|
- Parse trailing `#(\d+)` con regex. Tomar `max(N) + 1`. Si ninguno tiene número, devolver `1` solo si el prefijo bare existe (ya hay una sin número), de otro modo `null`.
|
||||||
|
- [ ] **18.2.4** Tests integración `packages/test-utils/tests/list-title-flow.test.ts`:
|
||||||
|
- LT-INT-01 admin add → suggestions incluyen el título.
|
||||||
|
- LT-INT-02 member add → P0002.
|
||||||
|
- LT-INT-03 computeNextNumber con 3 listas `Compra #1`, `Compra #2`, `Compra #5` → devuelve 6.
|
||||||
|
- LT-INT-04 computeNextNumber sin matches → null.
|
||||||
|
- [ ] **18.2.5** Tests unit `apps/web/src/lib/utils/list-title.test.ts` (extraer la regex + numbering en un módulo puro):
|
||||||
|
- LT-U-01 parse de "Compra #5" → `{ prefix: 'Compra', number: 5 }`.
|
||||||
|
- LT-U-02 parse de "Compra" → `{ prefix: 'Compra', number: null }`.
|
||||||
|
- LT-U-03 parse de "#5" (sin prefix) → no autonumera (returns null prefix).
|
||||||
|
- LT-U-04 nextNumber sobre lista de números `[1,2,5]` → 6.
|
||||||
|
- LT-U-05 nextNumber sobre lista vacía → 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 18.3 UI — create-list modal
|
||||||
|
|
||||||
|
**Ficheros:** ubicación actual de creación de lista (probablemente `apps/web/src/routes/(app)/lists/+page.svelte` o similar — auditar).
|
||||||
|
|
||||||
|
- [ ] **18.3.1** Input prefilled con `currentCollective.default_list_title || ''`.
|
||||||
|
- [ ] **18.3.2** Mientras el usuario escribe:
|
||||||
|
- Si el typed value matches un prefijo conocido + no incluye `#N`, mostrar un chip discreto `Sugerencia: Compra #6` (texto pequeño debajo del input, click rellena el input).
|
||||||
|
- Suggestion list aparece como dropdown con `fetchTitleSuggestions(prefix)`. Mismo patrón que `TagPicker` (Fase 11).
|
||||||
|
- [ ] **18.3.3** Submit deshabilitado si `name.trim() === ''`. Sin spinner cuando deshabilitado.
|
||||||
|
- [ ] **18.3.4** Si submit ocurre y el título matches un prefijo conocido sin `#N` y `computeNextNumber` devuelve un valor, **auto-suffix** el `#N` antes de insertar. Visible para el usuario en un toast no intrusivo: "Creada como `Compra #6`".
|
||||||
|
|
||||||
|
**Decisión UX:** auto-suffix solo si el typed value coincide con un título del catálogo. Para prefijos libres (no en catalog), respetar lo que el usuario escribió tal cual.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 18.4 UI — `/collective/manage` catálogo
|
||||||
|
|
||||||
|
**Fichero:** `apps/web/src/routes/(app)/collective/manage/+page.svelte`.
|
||||||
|
|
||||||
|
- [ ] **18.4.1** Nueva subsección "Títulos sugeridos" debajo de "Items frecuentes":
|
||||||
|
- Input `default_list_title` (text input, single-line, save on blur).
|
||||||
|
- Lista de títulos curados con botón "Borrar" por fila.
|
||||||
|
- Botón "Añadir título" + modal pequeño con input + Save.
|
||||||
|
- [ ] **18.4.2** Members ven la sección read-only (lista visible; sin acciones). Guests no la ven.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 18.5 Tests E2E
|
||||||
|
|
||||||
|
- [ ] **18.5.1** Playwright `tests/e2e/list-title-flow.test.ts`:
|
||||||
|
- **LTF-01** Ana setea `default_list_title = "Compra"` en manage; crea lista nueva → input prefilled "Compra"; submit → lista creada como "Compra" (no `#1` aún porque no había prefix-match en catalog antes).
|
||||||
|
- **LTF-02** Ana añade "Compra" al catálogo; crea 3 listas; verifica que la 3ª se nombra "Compra #2" automáticamente (la 1ª = "Compra", 2ª = "Compra #1" si #1 ausente; ajustar según `computeNextNumber` exact behavior).
|
||||||
|
- **LTF-03** Ana intenta crear con input vacío → botón disabled, no fire.
|
||||||
|
- **LTF-04** Borja (member) en manage ve la subsección con acciones disabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 18.Z Verificación + cierre
|
||||||
|
|
||||||
|
- [ ] **18.Z.1** `just test-all` verde. Suma esperada: +6 pgTAP + 4 integración + 5 unit + 4 e2e = +19.
|
||||||
|
- [ ] **18.Z.2** Smoke: crear 3 listas seguidas con prefijo común; verificar numbering correcto.
|
||||||
|
- [ ] **18.Z.3** Entrada en CHANGELOG: `[new] Required list titles + auto-numbered prefix + admin-curated title catalog (Fase 18).`
|
||||||
|
- [ ] **18.Z.4** Documentar en `docs/history/fase-18-shopping-list-flow.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Riesgos / notas
|
||||||
|
|
||||||
|
- **Race condition en numbering**: dos usuarios crean a la vez con prefijo `Compra`. Ambos clientes resuelven `computeNextNumber → 6`. Resultado: dos `Compra #6`. Mitigación posible: unique constraint `(collective_id, name)` — pero rompe casos donde es deliberado tener duplicados. Aceptar el race; documentar.
|
||||||
|
- **Caso "el usuario quiere típo libre"**: si el typed value no matches un prefix conocido, no auto-suffix. Asegurar que el chip de sugerencia solo aparece para matches del catálogo.
|
||||||
|
- **Default title sensible al colectivo activo**: al cambiar de colectivo en la sidebar, el modal create-list debe re-leer `currentCollective.default_list_title`. Reactivo vía store.
|
||||||
|
- **i18n**: el `#N` formato es universal; no traducir. Strings nuevos: `create_list_input_placeholder`, `create_list_suggestion_chip`, `create_list_auto_numbered_toast`, `manage_list_titles_section_title`, `manage_default_list_title_label`.
|
||||||
|
|
||||||
|
### Scope explícito fuera
|
||||||
|
|
||||||
|
- Plantillas de items (lista con items pre-cargados). Solo título.
|
||||||
|
- Numbering por mes/año (formato `Compra 2026-05`). Solo `#N` integer.
|
||||||
|
- Edición masiva del catálogo via CSV. Solo CRUD inline.
|
||||||
|
- Sugerencias cross-collective (títulos de otros colectivos del mismo usuario).
|
||||||
114
plan/fase-19-emoji-avatars.md
Normal file
114
plan/fase-19-emoji-avatars.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
### Fase 19 — Emoji avatars expansion (food + animal categories)
|
||||||
|
|
||||||
|
**v1.0 · 3/4.** Tercera fase del ciclo v1.0.
|
||||||
|
|
||||||
|
**Estado: 📋 Planificada.** Petición concreta: el avatar picker actual (`/settings`, también en colective-onboarding y `CreateCollectiveModal`) ofrece 8 emojis hardcoded. Ampliar con **todos los emojis oficiales** de food + animals según Unicode CLDR.
|
||||||
|
|
||||||
|
**Objetivo: agrupar emojis en categorías navegables (no scroll infinito flat), añadir food + animals completos (~270 glyphs combinados según Unicode 15), mantener performance del picker.**
|
||||||
|
|
||||||
|
**Decisiones clave:**
|
||||||
|
- Fuente de verdad: lista hardcoded en `apps/web/src/lib/data/emoji-catalog.ts`, no fetched. Generada una sola vez desde Unicode CLDR; commit del JSON. Tamaño ~5–10KB.
|
||||||
|
- Categorías visibles: **Caras** (mantiene los 8 actuales + ~30 más comunes), **Comida** (todos los food/drink), **Animales** (todos los animal/nature), **Objetos** (un pequeño set útil). El picker abre por defecto en "Caras"; pestañas en la cabecera.
|
||||||
|
- Sin emoji search en MVP de la fase — solo navegación por pestaña. Search si crece.
|
||||||
|
- Variantes de skin tone parked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 19.1 Catálogo
|
||||||
|
|
||||||
|
**Fichero:** nuevo `apps/web/src/lib/data/emoji-catalog.ts`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type EmojiCategory = 'faces' | 'food' | 'animals' | 'objects';
|
||||||
|
|
||||||
|
export const EMOJI_CATALOG: Record<EmojiCategory, readonly string[]> = {
|
||||||
|
faces: ['😀', '😎', '🥳', '🤓', '👻', '🤖', '👽', '🥸', /* ~30 más */],
|
||||||
|
food: ['🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', /* ~85 más, todo Unicode "Food & Drink" */],
|
||||||
|
animals: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', /* ~150 más, todo "Animals & Nature" */],
|
||||||
|
objects: ['🏠', '🏡', '🌳', '🌻', '⭐', '🔥', '🎁', '🎈', /* ~20 más */],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const CATEGORY_LABELS: Record<EmojiCategory, string> = {
|
||||||
|
faces: 'Caras',
|
||||||
|
food: 'Comida',
|
||||||
|
animals: 'Animales',
|
||||||
|
objects: 'Objetos',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tareas:**
|
||||||
|
|
||||||
|
- [ ] **19.1.1** Generar los arrays. Fuente recomendada: [unicode-emoji-json](https://github.com/muan/unicode-emoji-json) `data-by-group.json`. Copiar el bloque "Food & Drink" + "Animals & Nature". Filtrar variantes (eliminar items con ZWJ secuencias raras como `🐕🦺` si Vite no las renderiza consistente). Mantener Unicode 15 baseline (sin Unicode 16 que iOS aún no incluye).
|
||||||
|
- [ ] **19.1.2** Tamaño final esperado: ~280 strings, ~10KB del bundle. Verificar.
|
||||||
|
- [ ] **19.1.3** Generador deterministic — un script `scripts/build-emoji-catalog.mjs` ejecutable manualmente que rebuild el `.ts` desde una copia local del JSON. No corre en CI; solo cuando Unicode se actualiza.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 19.2 Componente EmojiPicker
|
||||||
|
|
||||||
|
**Fichero:** nuevo `apps/web/src/lib/components/EmojiPicker.svelte`.
|
||||||
|
|
||||||
|
- [ ] **19.2.1** Props: `selected: string | null`, `onSelect: (emoji: string) => void`, `onClose?: () => void`.
|
||||||
|
- [ ] **19.2.2** Layout: header con pestañas (4 botones), body grid `grid-cols-8` (responsive `sm:grid-cols-10`), cada celda 32×32 con `text-2xl`. Click selecciona + dispara `onSelect`.
|
||||||
|
- [ ] **19.2.3** Categoría seleccionada en `$state`, default `'faces'`. Switch instantáneo (no animación).
|
||||||
|
- [ ] **19.2.4** Lazy render: solo monta el grid de la categoría activa (no las 4 al mismo tiempo) — evita coste de ~280 nodos vacíos.
|
||||||
|
- [ ] **19.2.5** A11y: pestañas son `role="tab"`, grid `role="grid"`, cada celda `role="gridcell"` + aria-label con el codepoint name. Keyboard nav (arrow keys) entre celdas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 19.3 Integraciones
|
||||||
|
|
||||||
|
**Ficheros:**
|
||||||
|
- `apps/web/src/routes/(app)/settings/+page.svelte` (avatar del usuario).
|
||||||
|
- `apps/web/src/routes/(app)/onboarding/+page.svelte` (emoji inicial del colectivo).
|
||||||
|
- `apps/web/src/lib/components/CreateCollectiveModal.svelte` (emoji al crear).
|
||||||
|
- `apps/web/src/routes/(app)/collective/manage/+page.svelte` (emoji al renombrar — Fase 10.2).
|
||||||
|
|
||||||
|
- [ ] **19.3.1** Reemplazar los selectores hardcoded de emoji por `<EmojiPicker selected={...} onSelect={...} />`. Mantener el mismo padding/border del contenedor para no romper layouts.
|
||||||
|
- [ ] **19.3.2** Verificar que `users.avatar_emoji` y `collectives.avatar_emoji` siguen aceptando los nuevos glyphs (text column — sí).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 19.4 Tests
|
||||||
|
|
||||||
|
- [ ] **19.4.1** Vitest unit `apps/web/src/lib/components/EmojiPicker.test.ts`:
|
||||||
|
- EP-U-01 4 pestañas presentes.
|
||||||
|
- EP-U-02 default category = 'faces'.
|
||||||
|
- EP-U-03 click en una celda dispara `onSelect` con el emoji correcto.
|
||||||
|
- EP-U-04 cambiar pestaña re-renderiza el grid con la categoría correcta.
|
||||||
|
- EP-U-05 arrow keys mueven foco entre celdas.
|
||||||
|
- [ ] **19.4.2** Vitest unit `emoji-catalog.test.ts`:
|
||||||
|
- EC-U-01 `EMOJI_CATALOG.food.length >= 85`.
|
||||||
|
- EC-U-02 `EMOJI_CATALOG.animals.length >= 130`.
|
||||||
|
- EC-U-03 cada categoría no contiene duplicados.
|
||||||
|
- EC-U-04 no overlap entre categorías (un emoji solo en una).
|
||||||
|
- [ ] **19.4.3** Playwright `tests/e2e/emoji-picker.test.ts`:
|
||||||
|
- **EP-E-01** Ana en `/settings`, abre el picker, cambia a "Comida", selecciona 🍎, guarda → `users.avatar_emoji` actualizado, avatar renderiza 🍎.
|
||||||
|
- **EP-E-02** Onboarding: Eva (nuevo usuario) crea colectivo con emoji 🐱 desde "Animales".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 19.Z Verificación + cierre
|
||||||
|
|
||||||
|
- [ ] **19.Z.1** `just test-all` verde. Suma esperada: +9 unit + 2 e2e = +11.
|
||||||
|
- [ ] **19.Z.2** Smoke en iOS Safari + Android Chrome — verificar que TODOS los emojis del catálogo renderizan (no tofu □). Si algunos no, eliminar de la lista.
|
||||||
|
- [ ] **19.Z.3** Bundle-size check: bundle gz total no debe subir más de ~10KB respecto al baseline.
|
||||||
|
- [ ] **19.Z.4** Entrada en CHANGELOG: `[new] Emoji picker expanded with full food + animals categories (Fase 19).`
|
||||||
|
- [ ] **19.Z.5** Documentar en `docs/history/fase-19-emoji-avatars.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Riesgos / notas
|
||||||
|
|
||||||
|
- **iOS vs Android emoji rendering**: algunos Unicode 15 codepoints (🩷, 🩵, 🪿) aún no están en iOS 16 (sí en 17.4+). Filtrar a Unicode 14 si la base de usuarios incluye iOS < 17. Documentar la decisión.
|
||||||
|
- **CLDR shortName as aria-label**: para a11y, necesitamos el nombre del emoji ("red apple"). Si añadir el JSON completo de nombres infla el bundle, fallback a `aria-label={emoji}` (lee como "ÄPFel" en VoiceOver — feo pero funcional).
|
||||||
|
- **ZWJ sequences**: 👨👩👧 (familia) o 🏳️🌈 (rainbow flag) son secuencias múltiples. Los excluimos del catálogo en MVP de la fase (variantes de skin tone tampoco).
|
||||||
|
- **Performance del grid grande**: ~280 nodos en el DOM si renderizamos todas las pestañas. Por eso lazy-render por pestaña activa.
|
||||||
|
|
||||||
|
### Scope explícito fuera
|
||||||
|
|
||||||
|
- Buscar emoji por nombre.
|
||||||
|
- Skin tone variants.
|
||||||
|
- Recently-used (tracking de los últimos emojis del usuario).
|
||||||
|
- Custom emojis subidos por el colectivo.
|
||||||
|
- Sincronización del emoji con el sistema operativo del usuario.
|
||||||
136
plan/fase-20-euskera-locale.md
Normal file
136
plan/fase-20-euskera-locale.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
### Fase 20 — Euskera locale (eu)
|
||||||
|
|
||||||
|
**v1.0 · 4/4.** Última fase del ciclo v1.0. Tras cerrarla, se taggea `v1.0.0`.
|
||||||
|
|
||||||
|
**Estado: 📋 Planificada.** Añadir Euskera (Basque, ISO 639-1 `eu`) como tercer idioma soportado, sumándose a `en` y `es`. Tras esta fase la app está disponible para los hogares vasco-parlantes.
|
||||||
|
|
||||||
|
**Estado actual (auditado 2026-05-19):**
|
||||||
|
- Paraglide en `apps/web/messages/{en,es}.json`. Compile-time tree-shaken por idioma.
|
||||||
|
- Selector de idioma actual: `users.language text` (probablemente `en` | `es` por check constraint o convención). Auto-detect ya en Fase 10.8.
|
||||||
|
- ~300+ keys en cada locale (estimado por el tamaño actual).
|
||||||
|
|
||||||
|
**Decisiones clave:**
|
||||||
|
- **Machine-translate seed + flag for review (opción B):** generar `messages/eu.json` con traducción automática (DeepL-formatted-text o GPT-class) marcando cada valor con sufijo `[needs review]` para que la persona euskera-hablante pueda repasar y borrar el marcador.
|
||||||
|
- Soporte de idiomas declarados en la lista canónica: `apps/web/src/lib/i18n/supported.ts` (verificar nombre exacto, puede que viva en `paraglide` config). Añadir `'eu'`.
|
||||||
|
- `users.language` accept `'eu'` en el check constraint (si existe). Si no hay constraint, añadirlo ahora con 3 valores.
|
||||||
|
- Auto-detect (Fase 10.8): reconocer `eu` / `eu-ES` / `eu-FR` en `accept-language`.
|
||||||
|
- Selector visible: `/settings` ya tiene un control de idioma. Añadir opción "Euskera".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 20.1 Migración: language check
|
||||||
|
|
||||||
|
**Fichero:** `supabase/migrations/028_users_language_eu.sql`.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Drop the existing check (if it exists; some seeds may not have it).
|
||||||
|
alter table public.users
|
||||||
|
drop constraint if exists users_language_check;
|
||||||
|
|
||||||
|
-- Re-add with eu allowed.
|
||||||
|
alter table public.users
|
||||||
|
add constraint users_language_check
|
||||||
|
check (language in ('en','es','eu'));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tareas:**
|
||||||
|
|
||||||
|
- [ ] **20.1.1** Migración 028.
|
||||||
|
- [ ] **20.1.2** Tests pgTAP `supabase/tests/020_users_language_eu.sql`:
|
||||||
|
- update a `'eu'` accepted.
|
||||||
|
- update a `'fr'` rejected by check.
|
||||||
|
- [ ] **20.1.3** `just db-types`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 20.2 Locale file
|
||||||
|
|
||||||
|
**Fichero:** nuevo `apps/web/messages/eu.json`.
|
||||||
|
|
||||||
|
- [ ] **20.2.1** Pipeline de generación (manual, una vez):
|
||||||
|
1. Tomar `messages/es.json` como source-of-truth (lengua romance + base traductor más fiable hacia euskera que `en`).
|
||||||
|
2. Traducir cada valor con DeepL API o GPT con prompt "Translate to Euskera (Basque), keep ICU placeholders intact, preserve punctuation, formal register".
|
||||||
|
3. Marcar cada valor traducido con sufijo ` [needs review]` antes del cierre de la string. **Excepción**: las keys con placeholders ICU (`{name}`, `{count}`) llevan también el sufijo pero la review debe verificar que los placeholders no se hayan movido a posiciones gramaticalmente raras para euskera (SOV order vs SVO).
|
||||||
|
- [ ] **20.2.2** Validar el JSON: misma key set que `en.json` y `es.json`. Script `scripts/check-locales.mjs` que diff las keys y falla si faltan o sobran.
|
||||||
|
- [ ] **20.2.3** Bundle Paraglide rebuild: `pnpm --filter @colectivo/web exec paraglide-compile` (o cualquiera que sea el comando — verificar en `package.json`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 20.3 Wiring
|
||||||
|
|
||||||
|
**Ficheros:**
|
||||||
|
- `apps/web/src/lib/i18n/*` (configuración de Paraglide — locales soportados).
|
||||||
|
- `apps/web/src/lib/utils/accept-language.ts` (Fase 10.8 — añadir `'eu'` al set conocido).
|
||||||
|
- `apps/web/src/routes/(app)/settings/+page.svelte` (selector de idioma).
|
||||||
|
|
||||||
|
**Tareas:**
|
||||||
|
|
||||||
|
- [ ] **20.3.1** Añadir `'eu'` al array de idiomas en la config de Paraglide (`project.inlang/settings.json` u otro fichero del plugin).
|
||||||
|
- [ ] **20.3.2** `accept-language.ts` parser: reconocer `eu`, `eu-ES`, `eu-FR` → `'eu'`.
|
||||||
|
- [ ] **20.3.3** Selector de idioma en `/settings`: nueva opción "Euskera". Label en cada idioma:
|
||||||
|
- `en`: "Basque"
|
||||||
|
- `es`: "Euskera"
|
||||||
|
- `eu`: "Euskera"
|
||||||
|
- [ ] **20.3.4** Cambiar idioma a `eu` recarga Paraglide tags sin reload de página (`setLanguageTag('eu')`) + escribe `users.language = 'eu'`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 20.4 Tests
|
||||||
|
|
||||||
|
- [ ] **20.4.1** Vitest unit `apps/web/src/lib/utils/accept-language.test.ts` (extender):
|
||||||
|
- LANG-U-EU-01 `eu` → `'eu'`.
|
||||||
|
- LANG-U-EU-02 `eu-ES` → `'eu'`.
|
||||||
|
- LANG-U-EU-03 `eu-FR;q=0.5,en` → `'eu'` (preferencia primaria).
|
||||||
|
- [ ] **20.4.2** Vitest integración `language-bootstrap.test.ts` (extender):
|
||||||
|
- LANG-INT-EU-01 user setea language=eu, update OK.
|
||||||
|
- LANG-INT-EU-02 user intenta language='fr' → check constraint violation.
|
||||||
|
- [ ] **20.4.3** Playwright `tests/e2e/euskera.test.ts`:
|
||||||
|
- **EU-01** Ana cambia idioma a Euskera en `/settings` → strings nav cambian (Listas → "Erosketak", o lo que diga el JSON), sin reload.
|
||||||
|
- **EU-02** Eva (usuario nuevo) con header `Accept-Language: eu` → onboarding renderiza en Euskera.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 20.5 Review checklist (post-translation)
|
||||||
|
|
||||||
|
Antes del v1.0.0 release, abrir issue/asignar tarea para alguien euskera-hablante:
|
||||||
|
|
||||||
|
- [ ] **20.5.1** Repasar `messages/eu.json`, eliminar el sufijo ` [needs review]` de cada string corregido.
|
||||||
|
- [ ] **20.5.2** Para keys con plurales (`item_count_one`, `item_count_other` si existen), verificar que ambos son correctos (euskera no distingue como inglés/español; puede ser una sola forma).
|
||||||
|
- [ ] **20.5.3** Términos del dominio: "colectivo", "lista", "tarea", "nota", "compra". Acordar la traducción con el cliente (no asumir que la máquina la acertó).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 20.Z Verificación + cierre
|
||||||
|
|
||||||
|
- [ ] **20.Z.1** `just test-all` verde. Suma esperada: +2 pgTAP + 3 unit + 2 integración + 2 e2e = +9.
|
||||||
|
- [ ] **20.Z.2** Smoke manual: rotar las 3 pantallas top-level (`/lists`, `/tasks`, `/notes`) en cada idioma. Ninguna debe mostrar la key cruda (`__SKIPPED__` o el id).
|
||||||
|
- [ ] **20.Z.3** Bundle-size check: el chunk de `eu` es lazy-loaded por Paraglide; verificar que solo se carga cuando el user lo elige. No debe pesar más que `es.json`.
|
||||||
|
- [ ] **20.Z.4** Entrada en CHANGELOG: `[new] Euskera (Basque) locale (Fase 20). Machine-translated seed pending native review.`
|
||||||
|
- [ ] **20.Z.5** Documentar en `docs/history/fase-20-euskera-locale.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 20.ZZ v1.0.0 release
|
||||||
|
|
||||||
|
- [ ] **20.ZZ.1** Bump `package.json` version → `1.0.0`.
|
||||||
|
- [ ] **20.ZZ.2** Editar `CHANGELOG.md`: mover entradas de `[Unreleased]` a `## [v1.0.0] — YYYY-MM-DD`. Incluir resumen de las 4 fases del ciclo (17–20).
|
||||||
|
- [ ] **20.ZZ.3** `git tag -a v1.0.0 -m "v1.0.0 — versioning + shopping list flow + emoji avatars + euskera"`.
|
||||||
|
- [ ] **20.ZZ.4** Deploy desde el tag. Smoke en prod.
|
||||||
|
- [ ] **20.ZZ.5** Anuncio interno + nota sobre euskera review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Riesgos / notas
|
||||||
|
|
||||||
|
- **Calidad de la traducción automática**: euskera no es un idioma soportado de primera por DeepL (verificar fecha actual). GPT-4 class lo hace pero con errores. El sufijo `[needs review]` es explícito para no shipear texto incorrecto sin marcar.
|
||||||
|
- **ICU placeholders en euskera**: el orden de palabras (SOV) puede empujar `{name}` a posiciones no naturales en inglés/español. Revisar.
|
||||||
|
- **Selector ya soporta `system` (auto-detect)**: por la Fase 10.8 ya hay lógica de fallback. Asegurar que `eu` propaga por todo el árbol.
|
||||||
|
- **Test fixture users**: si los seeds (Ana, Borja, …) tienen `language='es'` hardcoded, ningún test rompe — solo los nuevos.
|
||||||
|
|
||||||
|
### Scope explícito fuera
|
||||||
|
|
||||||
|
- Galego, Català, Português, Francés. Solo Euskera en este release.
|
||||||
|
- Pluralización ICU avanzada (one/two/few/many/other) — euskera usa pluralización simple.
|
||||||
|
- Traducción de los strings de Keycloak (login screen). Ese es el operador del Keycloak externo.
|
||||||
|
- Localización del CHANGELOG. Solo en inglés.
|
||||||
|
- Localización de los nombres de los emojis (Fase 19). Aria-label sigue siendo el codepoint name CLDR en inglés.
|
||||||
Reference in New Issue
Block a user