Skip to content

feat(i18n): catálogos es/en y selector de idioma con bandeja y títulos nativos (L2/L3 #66) - #163

Merged
egdev6 merged 5 commits into
egdev6:mainfrom
danielgap:feat/i18n-catalogs-core
Oct 21, 2026
Merged

egdev6 merged 5 commits into
egdev6:mainfrom
danielgap:feat/i18n-catalogs-core

Conversation

@danielgap

Copy link
Copy Markdown
Contributor

Related issue / Issue relacionada

Refs #66

ODD: fase L2/L3, núcleo: catálogos y selector global. Sigue a #152 (L1) y #159 (ids estables). Gate de #66: "Tests de claves/fallback/sincronización y selección persistente".

Scope / Alcance

El idioma se elige en Ajustes → Apariencia → Idioma (Español / English). Se guarda al momento y cambia sin reiniciar:

  • en las ventanas de la app, el menú lateral de Ajustes y los títulos de sus páginas;
  • en Rust, el menú de la bandeja, los títulos nativos de las ventanas (Ajustes, detalle y efectos) y el diálogo de instalación de Codex.

El resto de textos se migra por áreas en los siguientes PRs (T2c). Con el idioma en es, todo se ve igual que hoy.

Son dos commits revisables por separado:

1. Rust: feat(i18n): switch tray, window titles and Codex dialog by locale

  • src-tauri/src/i18n.rs: Locale (cualquier valor desconocido se trata como es) y enum Msg con match exhaustivo es/en. Los textos en español son los literales actuales, copiados tal cual, y un test lo comprueba.
  • Al guardar un idioma distinto, save_config y patch_config reconstruyen la bandeja (set_menu, mismos ids y handlers) y re-titulan las ventanas abiertas (set_title). Al arrancar y al crear ventanas se usa el idioma configurado.
  • La decisión (si cambió el idioma, qué etiquetas y qué títulos) es una función pura, locale_relabel, y se ejecuta con efectos inyectados (run_locale_relabel). Así se testea sin Tauri.
  • Seguimientos de feat(i18n): ids estables de medidores entre Rust y JS (L2 #66) #159:
    • (1) Meter.id queda documentado como obligatorio. El historial hace debug_assert! si falta, y el fallback a la etiqueta para datos antiguos vive en una función pura, sample_key.
    • (2) Los medidores de anthropic_api, openai_api y copilot salen de funciones puras con test (budget_meter, premium_meter).

2. TS: feat(i18n): typed es/en catalogs and language selector in Settings

  • src/i18n/es.ts: hecho con los literales existentes, sin retraducir (matiz 4 de L0). en.ts usa satisfies Messages, así que una clave que falte o sobre no compila.
  • t(locale, key, params?) acepta solo claves válidas, interpola {placeholder} y, si falta un texto, cae así: locale → es → la propia clave. intlLocale es un mapa cerrado es→es-ES, en→en-US.
  • Tests de runtime: todas las claves existen en los dos idiomas y los placeholders coinciden.
  • useLocale() (Ajustes, detalle): lee la config y escucha config-changed. Vuelve a leer la config, porque el payload a veces es (). El widget usa su propia config, que ya recibe ese evento.
  • Selector: guarda por la misma cola que el resto de Ajustes. El borrador del formulario también adopta el idioma; si no, aparecía "cambios sin guardar" y "Guardar cambios" devolvía el idioma anterior. Está cubierto con test.
  • docs/i18n-inventory.md regenerado.

Fuera de alcance: el resto de textos de React y Rust (T2c), los formatos y los errores propios (L4), y message/plan/periodLabel de los proveedores (L4).

Tamaño: unas 1050 líneas escritas con los tests (Rust ~550, TS ~560), más el inventario generado. Lo mantengo en un PR porque el cambio de idioma solo se puede probar completo con las dos partes. Si prefieres revisarlo en dos, lo separo.

Coordination / Coordinación

Contribuidor: @danielgap

Acceptance and validation / Aceptación y validación

  • Criterios cumplidos, de feat: interfaz bilingüe ES/EN con español por defecto #66: "Selector en Ajustes actualiza todas las ventanas y textos nativos" (para los textos ya migrados) y la selección es persistente.
  • Comprobaciones ejecutadas (Windows 10):
    • Tests primero:
      • Rust: los tests de i18n, del historial y de los medidores extraídos fallaron antes de implementar. Los de locale_relabel no compilaban (E0425) antes de existir la función.
      • TS: los tests de catálogos, del hook y de la navegación fallaron porque los módulos no existían. changeLocale falló con "is not a function".
    • cargo test: 251/251.
    • npx vitest run --dir src: 651/651.
    • npx tsc --noEmit: OK.
    • node scripts/i18n-inventory.mjs --check: OK.
    • Prueba manual con npm run app:dev. Al cambiar a English:
      • el menú lateral y el título de Ajustes cambian al momento;
      • la bandeja (clic derecho) muestra "Show / hide widget", "Refresh now", "Settings…", "Quit";
      • "Guardar cambios" no muestra el aviso de cambios sin guardar ni revierte el idioma;
      • vuelta a Español correcta.
  • Comprobaciones no ejecutadas y motivo:
    • Que Tauri pinte el texto nuevo no tiene test automático. En tauri 2.12.1, MockRuntime::set_title no hace nada y title() devuelve "", y mock_context arranca sin bandeja. Por eso se testea la decisión, y lo pintado se probó a mano.
    • El onChange del <select> no tiene test de componente, porque el repo no usa jsdom ni testing-library. La lógica está en changeLocale, que es pura y tiene test. Si te parece bien añadir esas devDependencies, puedo proponerlo aparte.
    • Prueba en Linux y macOS: no disponible.
  • Bloqueos pendientes: ninguno.

…dev6#66)

Add src-tauri/src/i18n.rs: a Locale parsed from the config (unknown -> es)
and a Msg enum with an exhaustive es/en match. Spanish texts are the
existing literals verbatim. The tray menu and the settings, detail and
overlay window titles are rebuilt when a saved config changes locale,
and use the configured locale at startup; the Codex consent dialog reads
it when shown.

Maintainer follow-ups from egdev6#159: Meter.id is documented as required and
history asserts it in debug builds (legacy label fallback kept in a pure
sample_key); the meters built inside anthropic_api, openai_api and
copilot fetches are now pure, unit-tested constructors.
…gdev6#66)

Add src/i18n: `es` built from the existing Spanish literals, `en` checked
with `satisfies` so a missing or extra key fails to compile, `t()` with
typed keys, {placeholder} interpolation and a locale -> es -> key
fallback, a closed Intl locale map, and runtime tests for key presence
and placeholder parity.

Settings gets a "Idioma" selector in Apariencia that saves through the
existing queue and broadcasts config-changed; the draft adopts the new
locale so "Guardar cambios" cannot revert it. Settings and DetailPanel
follow the locale with useLocale(); the Settings navigation and page
titles are the first texts migrated.
Copilot AI balanced review requested due to automatic review settings October 21, 2026 13:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@egdev6

egdev6 commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Gracias, @danielgap. He revisado 0764048 y veo dos puntos que conviene corregir antes de fusionar:

  1. P1 · CI bloqueada en Windows, Linux y macOS. Los fixtures de src-tauri/src/i18n.rs:257–264 incluyen Quorb Dev y quorb-dev, que detecta el guard de retirada de ese canal. Lo reproduje con node --test scripts/dev-retirement.test.mjs; es también el fallo de las tres CI. Usa una identidad ficticia distinta para probar las sustituciones, sin relajar el guard.
  2. P2 · Idioma pendiente invisible si falla el guardado. changeLocale (src/i18n/use-locale.ts:28–31) cambia el borrador antes de persistir, pero el selector (src/Settings.tsx:2225) muestra el locale del backend y el error solo llega a consola (:1377). Inyectando un fallo de persistencia, el borrador queda en en y el backend en es: el selector muestra Español, pero «Guardar cambios» puede aplicar después English. Conviene reconciliar el selector y el borrador al fallar, mostrar el error y cubrir ese caso con un test.

Validación: 651 tests frontend, TypeScript, build y 7 tests aislados del catálogo Rust pasan. La suite de scripts pasa 220/221, con el guard anterior como único fallo. No ejecuté cargo test completo ni pruebas GUI.

…cale save (egdev6#66)

- Use the fictional Quorb Beta channel/folder in the Rust catalog test
  instead of the retired Dev identity, so the dev-retirement guard passes
  without being relaxed (P1, review of egdev6#163).
- changeLocale reverts the Settings draft to the persisted backend locale
  when the immediate save fails and rethrows; Settings shows the error next
  to the language selector instead of console-only logging (P2, review of egdev6#163).
- Cover the reconciliation and the double-failure rethrow with unit tests
  and add settings.language.error to the es/en catalogs.
@danielgap

Copy link
Copy Markdown
Contributor Author

¡Gracias por la revisión tan completa! Ambos puntos quedaron resueltos en 7bc6ab3.

  • P1: los fixtures del test de Rust usan ahora la identidad ficticia «Quorb Beta» / .quorb-beta, así que el guard de retirada pasa sin relajarse: node --test scripts/dev-retirement.test.mjs 5/5 y cargo test i18n 7/7.
  • P2: changeLocale devuelve el borrador al locale que realmente persistió el backend cuando el guardado falla (conservando el resto de ediciones) y relanza el error; Ajustes lo muestra junto al selector con la clave nueva settings.language.error en ambos catálogos. Cubierto con dos tests de unidad en use-locale.test.ts.

Verifiqué además la suite completa en local: vitest 653/653, tsc --noEmit limpio y suite de scripts verde salvo setup-signing.test.mjs, que ya fallaba en el commit base por EPERM de symlinks en Windows (limitación del entorno local, no del cambio). La CI del PR está corriendo ahora.

@egdev6 egdev6 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verificado en local sobre 7bc6ab3 (rama actualizada a pr-163):

  • P1 ✓ — Los fixtures del test de Rust usan ahora «Quorb Beta» / .quorb-beta (identidad ficticia, no la retirada Dev) y el guard no se relaja: node --test scripts/dev-retirement.test.mjs 5/5 y cargo test i18n 7/7 en verde.
  • P2 ✓ — changeLocale relee el locale persistido del backend al fallar el guardado, devuelve el borrador a ese locale conservando el resto de ediciones y relanza el error; Ajustes lo muestra junto al selector con settings.language.error en ambos catálogos. Los dos tests nuevos de use-locale.test.ts cubren la reconciliación y el rethrow del doble fallo. tsc --noEmit limpio y suite completa verde (653/653 antes del merge).

Un detalle nuevo tras el merge con main (e2cf99b): docs/i18n-inventory.md quedó desactualizado — node scripts/i18n-inventory.mjs --check sale con código 1 en local (los tree hashes del encabezado no coinciden con los árboles del merge) y de hecho el job Tests and build (Windows) de la CI falló justo en ese paso. Basta regenerarlo en la rama:

node scripts/i18n-inventory.mjs --out docs/i18n-inventory.md

Con ese commit el PR quedaría listo para aprobar por mi parte.

@egdev6

egdev6 commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Regenerado docs/i18n-inventory.md en 6eb163e tras el merge con main: node scripts/i18n-inventory.mjs --check pasa en local y la CI del PR está verde en los cuatro workflows (CI, Linux PR CI, Code safety y macOS CI, además de Security). Por mi parte, el PR queda listo para fusionar.

@egdev6
egdev6 merged commit e781046 into egdev6:main Oct 21, 2026
5 checks passed
Rafaeldelinares added a commit to Rafaeldelinares/quorb that referenced this pull request Oct 22, 2026
Atiende la review de egdev6#169 y los puntos 2–5 acordados en egdev6#168.

Integra `main` (ya con egdev6#163) y corrige el diff anterior:

- `createWidgetRecovery` (egdev6#148) queda acotado a Windows. En `xdg-shell` el
  compositor, por diseño, no expone la posición absoluta de una ventana
  cliente y `outerPosition()` devuelve (0, 0): el ciclo de 5 s concluía que
  la ventana estaba descolocada y la reubicaba.

- El muestreo de presencia (egdev6#154) queda acotado a macOS. El PR asumía que en
  Wayland `cursorPosition()` fallaría; en WebKitGTK no falla, devuelve
  (0, 0), así que cada muestra concluía «cursor fuera» y cerraba el panel al
  instante. Linux vuelve a cerrar por los eventos del DOM.

- El asa arrancaba su aritmética por delta desde `outerPosition()`, que en
  Wayland devuelve (0, 0). Ahora arranca desde `widgetBounds.current`, que es
  la verdad incluso en el caso legítimo (0, 0) porque lo escribe el propio
  `setWidgetBounds` que colocó la ventana, no una lectura del compositor.
  Queda documentado en el código. Al soltar, el imán usa el desplazamiento
  acumulado cuando el compositor no reporta posición.

- `OPEN_DELAY` pasa a 350 ms solo fuera de Windows y macOS. En Linux el
  barrido del puntero no tiene más red de contención que el temporizador,
  porque el muestreo nativo es solo de macOS; en Windows y macOS los 90 ms
  originales se conservan y egdev6#164 ya mitiga la reapertura por foco. El toggle
  por clic sale del PR y se propone como issue de producto aparte.

- `styles.css` declara el fondo transparente también en `#root` y `.dock`. El
  refactor `:is(...)` y el scoping `data-auto-hide` quedan revertidos:
  cambiaban la ocultación con auto-ocultar desactivado en cualquier
  plataforma y son fuera de alcance.

`tauri.conf.json` y `ProviderGauge.tsx` vuelven a quedar idénticos a `main`.
`noRedirectionBitmap` es opción solo de Windows (en `tao 0.37.1` hay cuatro
referencias, todas en `platform_impl/windows/`, y ninguna en `linux/` ni
`wayland/`), y no puede trasladarse a `tauri.windows.conf.json`: `tauri-utils`
fusiona la config de plataforma con JSON Merge Patch (RFC 7396), que
reemplaza los arrays completos, así que un `app.windows` en el override se
tragaría la definición entera de la ventana en Windows.
egdev6 pushed a commit that referenced this pull request Oct 23, 2026
…rrastre en Wayland (#168) (#169)

* fix(linux): resolve wayland transparency, drag movement and hover regressions (#107)

Quorb v0.7.0 introduced multiplatform improvements (#143, #148, #154) that inadvertently broke Linux/Wayland due to differences in desktop protocols and WebKitGTK:
- Window transparency: `noRedirectionBitmap: true` in tauri.conf.json bypassed compositing redirection buffers, rendering the widget as an opaque black box on Wayland compositors (KDE KWin, Mutter).
- Widget disappearing / flickering: Windows DWM recovery loop (#148) queried `outerPosition()`, which Wayland xdg-shell intentionally reports as (0, 0), causing infinite repositioning loops. Guarded with `/Windows/.test(navigator.userAgent)`.
- Premature popup closing: Detail presence polling (#154) queried `cursorPosition()`, which on Linux/GTK returns (0, 0) instead of throwing. Comparing (0, 0) against window geometry triggered immediate close timers every 400ms. Guarded with `/Macintosh/.test(navigator.userAgent)`.
- Drag movement freeze: Drag start now initializes synchronously from `widgetBounds.current` and calculates move deltas from pointer offsets, avoiding freezes when window coordinates report (0, 0).
- Hover ergonomics: Raised `OPEN_DELAY` from 90ms to 350ms to prevent accidental triggers, and enabled direct click-to-open/toggle on provider gauges.

* fix(linux): acotar recoveries por plataforma y desbloquear el arrastre

Atiende la review de #169 y los puntos 2–5 acordados en #168.

Integra `main` (ya con #163) y corrige el diff anterior:

- `createWidgetRecovery` (#148) queda acotado a Windows. En `xdg-shell` el
  compositor, por diseño, no expone la posición absoluta de una ventana
  cliente y `outerPosition()` devuelve (0, 0): el ciclo de 5 s concluía que
  la ventana estaba descolocada y la reubicaba.

- El muestreo de presencia (#154) queda acotado a macOS. El PR asumía que en
  Wayland `cursorPosition()` fallaría; en WebKitGTK no falla, devuelve
  (0, 0), así que cada muestra concluía «cursor fuera» y cerraba el panel al
  instante. Linux vuelve a cerrar por los eventos del DOM.

- El asa arrancaba su aritmética por delta desde `outerPosition()`, que en
  Wayland devuelve (0, 0). Ahora arranca desde `widgetBounds.current`, que es
  la verdad incluso en el caso legítimo (0, 0) porque lo escribe el propio
  `setWidgetBounds` que colocó la ventana, no una lectura del compositor.
  Queda documentado en el código. Al soltar, el imán usa el desplazamiento
  acumulado cuando el compositor no reporta posición.

- `OPEN_DELAY` pasa a 350 ms solo fuera de Windows y macOS. En Linux el
  barrido del puntero no tiene más red de contención que el temporizador,
  porque el muestreo nativo es solo de macOS; en Windows y macOS los 90 ms
  originales se conservan y #164 ya mitiga la reapertura por foco. El toggle
  por clic sale del PR y se propone como issue de producto aparte.

- `styles.css` declara el fondo transparente también en `#root` y `.dock`. El
  refactor `:is(...)` y el scoping `data-auto-hide` quedan revertidos:
  cambiaban la ocultación con auto-ocultar desactivado en cualquier
  plataforma y son fuera de alcance.

`tauri.conf.json` y `ProviderGauge.tsx` vuelven a quedar idénticos a `main`.
`noRedirectionBitmap` es opción solo de Windows (en `tao 0.37.1` hay cuatro
referencias, todas en `platform_impl/windows/`, y ninguna en `linux/` ni
`wayland/`), y no puede trasladarse a `tauri.windows.conf.json`: `tauri-utils`
fusiona la config de plataforma con JSON Merge Patch (RFC 7396), que
reemplaza los arrays completos, así que un `app.windows` en el override se
tragaría la definición entera de la ventana en Windows.

* docs(i18n): regenerar el inventario sobre el árbol ya mergeado

El header graba `git rev-parse HEAD:src`; generado antes del commit, no
coincidía con el árbol resultante y el gate de Windows lo rechazaba.

* refactor(frontend): one tested osFromUA helper and two named platform gates

The three inline UA regexes in Widget.tsx (OPEN_DELAY, the DWM recovery
audit of #148, the cursor presence sampling of #154) are now a single
injected-UA helper, and each gate is a named, tested predicate next to the
logic it guards. Behaviour is unchanged: the helpers keep the exact
predicates they replace, and a regression test pins each one against the
legacy regex it replaced.

Follows the review of #169 (suggestions 1 and 2).

* docs(frontend): annotate the three !important and the (0,0) drag branch

Records the two notes the #169 review asked to keep with the code: the
three `!important` that make the transparent widget work under WebKitGTK
are the first candidates to remove if the cascade is ever refactored, and
a release from a real (0,0) corner takes the delta branch whose only
possible deviation is compositor rounding.

Regenerates docs/i18n-inventory.md over the merged tree (#169 review,
point 4).

* fix(i18n): regenerate the text inventory over the committed tree

The previous regeneration ran before the commit that changed src/, so the
header recorded the previous commit's src tree hash and the gate failed on
the Windows job. The file is the only thing this commit changes, so the
header stays valid. Verified with core.autocrlf=true to match the Windows
checkout.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants