Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composable-os/src/i18n/english-portal-builder.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe("PortalBuilder sessions in English", () => {
expect(screen.getByRole("tab", { name: "Page builder" })).toBeInTheDocument();
expect(await screen.findByText("Ana Torres")).toBeInTheDocument();
expect(screen.getByText("Pending")).toBeInTheDocument();
expect(screen.getByText(`[email protected] · expires ${formatDate("2026-10-05T12:00:00Z", "en", "short")}`)).toBeInTheDocument();
expect(screen.getByText(`[email protected] · expires ${formatDate("2026-10-05T12:00:00Z", "en", "medium")}`)).toBeInTheDocument();
expect(screen.queryByText("Pendiente")).not.toBeInTheDocument();
});

Expand Down
9 changes: 9 additions & 0 deletions composable-os/src/i18n/english-workflows.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ describe("Workflows in English", () => {
expect(screen.getByText(formatDate("2026-09-30T12:00:00Z", "en", "short"))).toBeInTheDocument();
});

it("names the send-email action instead of showing its code", async () => {
svc.getAll.mockResolvedValue([workflow({ actions: [{ type: "send_email", config: {} }] })]);
renderPage(await english());
await screen.findByText("Lead intake");

expect(screen.getByText("send email")).toBeInTheDocument();
expect(screen.queryByText("send_email")).not.toBeInTheDocument();
});

it("explains an empty list and offers to create the first workflow", async () => {
svc.getAll.mockResolvedValue([]);
renderPage(await english());
Expand Down
3 changes: 2 additions & 1 deletion composable-os/src/locales/en/workflows.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"event": "event"
},
"actionType": {
"log": "log"
"log": "log",
"send_email": "send email"
}
}
3 changes: 2 additions & 1 deletion composable-os/src/locales/es/workflows.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"event": "evento"
},
"actionType": {
"log": "log"
"log": "log",
"send_email": "enviar correo"
}
}
2 changes: 1 addition & 1 deletion composable-os/src/pages/PortalBuilder.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe("PortalBuilder: sesiones, en español", () => {
expect(screen.getByRole("tab", { name: "Constructor de páginas" })).toBeInTheDocument();
expect(await screen.findByText("Ana Torres")).toBeInTheDocument();
expect(screen.getByText("Pendiente")).toBeInTheDocument();
expect(screen.getByText(`[email protected] · expira ${formatDate("2026-10-05T12:00:00Z", "es", "short")}`)).toBeInTheDocument();
expect(screen.getByText(`[email protected] · expira ${formatDate("2026-10-05T12:00:00Z", "es", "medium")}`)).toBeInTheDocument();
});

it("agrees the session count in the singular and the plural", async () => {
Expand Down
2 changes: 1 addition & 1 deletion composable-os/src/pages/PortalBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,7 @@ function PortalSessions() {
{statusLabel(s.action, s.accessed_at)}
</div>
<p className="text-xs text-muted-foreground mt-0.5 truncate">
{t("portalBuilder:sessions.expires", { email: s.client_email ?? t("portalBuilder:sessions.noEmail"), date: formatDate(s.expires_at, "short") })}
{t("portalBuilder:sessions.expires", { email: s.client_email ?? t("portalBuilder:sessions.noEmail"), date: formatDate(s.expires_at, "medium") })}
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
Expand Down
4 changes: 2 additions & 2 deletions composable-os/src/pages/QuoteDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,8 @@ export default function QuoteDetail() {
<div className="flex items-center justify-between px-4 py-2.5 rounded-lg border border-emerald-300/40 bg-emerald-500/10 text-sm">
<span className="text-emerald-400 text-xs">
{activeSession.client_email
? t("sales:detail.activeLinkSentTo", { email: activeSession.client_email, date: formatDate(activeSession.expires_at, "short") })
: t("sales:detail.activeLink", { date: formatDate(activeSession.expires_at, "short") })}
? t("sales:detail.activeLinkSentTo", { email: activeSession.client_email, date: formatDate(activeSession.expires_at, "medium") })
: t("sales:detail.activeLink", { date: formatDate(activeSession.expires_at, "medium") })}
</span>
<button
className="text-blue-400 text-xs hover:underline"
Expand Down
2 changes: 1 addition & 1 deletion docs/IMPLEMENTATION_ALIGNMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Legacy documents are historical reference only unless explicitly promoted by ADR
- Public token-based external interaction already exists in production through Portal
- Business events are emitted through the shared event envelope in `backend/app/events/types.py`
- Domain errors are emitted through the shared error envelope in `backend/app/core/exceptions.py`. Every module and `core/deps.py` raise registered codes; no `HTTPException` with free-text `detail` remains in `backend/app/modules/` or the auth dependencies. CI enforces parity between raised codes, `docs/ERROR_CODE_REGISTRY_V1.md`, and the frontend error catalogue, so the user-facing wording lives in `composable-os/src/locales/<lang>/errors.json` (resolved by `src/lib/errors.ts`) per ADR 0014 and ADR 0015
- Currency, number and date formatting in the frontend goes through one module, `composable-os/src/i18n/format.ts` (`useFormat()` in components), driven by the active locale; an ESLint rule (`no-restricted-syntax`) rejects `toLocale*String` and `Intl.NumberFormat`/`DateTimeFormat` anywhere else. A language without a region formats with the product's default region (`es` → `es-MX`), so adopting the active locale does not change any rendered number. See `docs/I18N_CONVENTIONS.md`
- Currency, number and date formatting in the frontend goes through one module, `composable-os/src/i18n/format.ts` (`useFormat()` in components), driven by the active locale; an ESLint rule (`no-restricted-syntax`) rejects `toLocale*String` and `Intl.NumberFormat`/`DateTimeFormat` anywhere else. A language without a region formats with the product's default region (`es` → `es-MX`), so adopting the active locale does not change any rendered number. Dates that must read unambiguously in any language (the portal-link expiry in `QuoteDetail` and `PortalBuilder`) use the `medium` style (`10 dic 2026`), not the numeric short one. See `docs/I18N_CONVENTIONS.md`
- Frontend pages migrated to i18n (`Login`, `Register`, `Workflows`, `Index`, `Discovery`, `Settings`, `Analytics`, `CompanyProfileSettings`, `CustomerPortal`, `NotFound`, `Contracts`, `CRM`, `Sales`, `QuoteDetail`, `HR`, `InventoryOrders`, `Projects`, `Invoicing`, `PortalBuilder`, plus the app shell (`AppSidebar`, `AppLayout`) and the shared `ui/dialog`, `ui/sheet` and `ui/sidebar`) are guarded by `src/test/migrated-pages.test.ts`: a static AST scan fails the build if visible text is hard-coded in any of them again. Each page also has a pseudo-localised render test (`EN(...)`). Every page in the i18n plan is now migrated; what remains is the backend (catalogues, invoice PDFs, emails) and a real second language, tasks 10-12 of the i18n plan

### Partially Aligned
Expand Down
8 changes: 5 additions & 3 deletions docs/superpowers/plans/2026-08-07-i18n-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,11 @@ completa migrada no hay patron que seguir.
desarrollador y es la reserva. Los eventos ya guardados conservan su texto en
espanol salvo los que no necesitan datos (enviada, aceptada, lotes), que se
traducen igualmente; reescribir el resto es una migracion de datos aparte.
- [ ] Pendientes del recorrido: etiqueta de `send_email`, fecha corta del vencimiento del
enlace, Discovery (asistente, blueprint, capacidades) y repetir el recorrido con
navegador cuando Chrome alcance el entorno.
- [x] Los otros dos hallazgos: `workflows:actionType.send_email` (es y en) y el vencimiento
del enlace del portal (`QuoteDetail`, `PortalBuilder`) con fecha `medium`
(`10 dic 2026`), no la numerica corta.
- [ ] Pendientes del recorrido: Discovery (asistente, blueprint, capacidades) y repetir el
recorrido con navegador cuando Chrome alcance el entorno.

---

Expand Down
Loading