Repository navigation
feat(i18n): settings en ingles (tarea 12, 18/n) #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| import { fireEvent, screen, waitFor } from "@testing-library/react"; | ||
| import i18next, { type i18n as I18n } from "i18next"; | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| import { buildI18nOptions, resources } from "@/i18n"; | ||
| import Settings from "@/pages/Settings"; | ||
| import { renderPageWithI18n } from "@/test/i18n"; | ||
|
|
||
| /** | ||
| * `settings` en el ingles *real* (tarea 12 del plan de i18n): las cinco pestanas de | ||
| * Ajustes con el catalogo `en` que se envia, no el pseudo-localizado de | ||
| * `Settings.test.tsx`. | ||
| */ | ||
|
|
||
| const svc = vi.hoisted(() => ({ getHealth: vi.fn() })); | ||
| const http = vi.hoisted(() => ({ get: vi.fn(), put: vi.fn() })); | ||
|
|
||
| vi.mock("@/services/health", () => ({ healthService: svc })); | ||
| vi.mock("@/lib/api", async (importOriginal) => ({ | ||
| ...(await importOriginal<typeof import("@/lib/api")>()), | ||
| api: http, | ||
| })); | ||
|
|
||
| async function english(): Promise<I18n> { | ||
| const instance = i18next.createInstance(); | ||
| await instance.init(buildI18nOptions(resources, "en")); | ||
| return instance; | ||
| } | ||
|
|
||
| const health = (over: Record<string, unknown> = {}) => ({ | ||
| status: "healthy", | ||
| version: "1.2.3", | ||
| checks: [ | ||
| { name: "api", status: "healthy", latency_ms: 12 }, | ||
| { name: "postgres", status: "degraded", latency_ms: 340 }, | ||
| ], | ||
| ...over, | ||
| }); | ||
|
|
||
| const prefs = (over: Record<string, unknown> = {}) => ({ | ||
| email_enabled: true, | ||
| email_events: { QuoteAccepted: true, SalesOrderCreated: true, WorkflowFailed: true, InventoryLowThresholdDetected: false, InvoiceOverdue: false }, | ||
| ...over, | ||
| }); | ||
|
|
||
| beforeEach(() => { | ||
| svc.getHealth.mockReset().mockResolvedValue(health()); | ||
| http.get.mockReset().mockResolvedValue(prefs()); | ||
| http.put.mockReset().mockImplementation(async (_url: string, patch: { email_enabled?: boolean; email_events?: Record<string, boolean> }) => { | ||
| const current = prefs(); | ||
| return { ...current, ...patch, email_events: { ...current.email_events, ...(patch.email_events ?? {}) } }; | ||
| }); | ||
| }); | ||
|
|
||
| /** Radix activa una pestana con `mousedown`, no con `click`. */ | ||
| const openTab = (name: string | RegExp) => fireEvent.mouseDown(screen.getByRole("tab", { name }), { button: 0 }); | ||
|
|
||
| describe("Settings in English", () => { | ||
| it("renders the header and the five tabs", async () => { | ||
| renderPageWithI18n(<Settings />, await english()); | ||
|
|
||
| expect(screen.getByRole("heading", { name: "Settings" })).toBeInTheDocument(); | ||
| expect(screen.getByText("Platform configuration, architecture and system status.")).toBeInTheDocument(); | ||
| for (const tab of ["Architecture", "System health", "Notifications", "General", "Team"]) { | ||
| expect(screen.getByRole("tab", { name: tab })).toBeInTheDocument(); | ||
| } | ||
| }); | ||
|
|
||
| it("shows the diagram and the three layer cards in English", async () => { | ||
| renderPageWithI18n(<Settings />, await english()); | ||
|
|
||
| expect(screen.getByText("Platform architecture")).toBeInTheDocument(); | ||
| expect(screen.getByText(/Hover over the components/)).toBeInTheDocument(); | ||
| for (const text of ["Client apps", "Graph database", "Vector database", "AI agents", "API LAYER", "MICROSERVICES", "DATA LAYER", "HTTPS"]) { | ||
| expect(screen.getByText(text)).toBeInTheDocument(); | ||
| } | ||
| expect(screen.getAllByText("Microservice")).toHaveLength(6); | ||
| expect(screen.getByText("Data layer")).toBeInTheDocument(); | ||
| expect(screen.getByText(/Asynchronous pub\/sub messaging/)).toBeInTheDocument(); | ||
| expect(screen.getByText(/Handles authentication, rate limiting/)).toBeInTheDocument(); | ||
| expect(screen.queryByText(/Microservicio|CAPA DE/)).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| describe("system health", () => { | ||
| it("shows each service with an English status and the latency", async () => { | ||
| renderPageWithI18n(<Settings />, await english()); | ||
| openTab("System health"); | ||
|
|
||
| expect(await screen.findByText("healthy", { selector: "div" })).toBeInTheDocument(); | ||
| expect(screen.getByText("degraded")).toBeInTheDocument(); | ||
| expect(screen.getByText("12ms")).toBeInTheDocument(); | ||
| expect(screen.getByText("340ms")).toBeInTheDocument(); | ||
| expect(screen.getAllByText("Latency")).toHaveLength(2); | ||
| }); | ||
|
|
||
| it("shows the overall status, the version and how fresh the data is", async () => { | ||
| renderPageWithI18n(<Settings />, await english()); | ||
| openTab("System health"); | ||
|
|
||
| expect(await screen.findByText(/Overall status:/)).toBeInTheDocument(); | ||
| expect(screen.getByText(/v1\.2\.3/)).toBeInTheDocument(); | ||
| expect(await screen.findByText(/^Updated (now|\d+ ?s(ec\.)? ago)$/)).toBeInTheDocument(); | ||
| expect(screen.getByRole("button", { name: "Refresh" })).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("explains when the backend cannot be reached", async () => { | ||
| svc.getHealth.mockRejectedValue(new Error("down")); | ||
| renderPageWithI18n(<Settings />, await english()); | ||
| openTab("System health"); | ||
|
|
||
| expect(await screen.findByText("Could not connect to the system. Check that the backend is running.", undefined, { timeout: 5000 })).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("notifications", () => { | ||
| it("lists the event types in English with named switches and a summary", async () => { | ||
| renderPageWithI18n(<Settings />, await english()); | ||
| openTab("Notifications"); | ||
|
|
||
| expect(await screen.findByText("Quotes accepted")).toBeInTheDocument(); | ||
| expect(screen.getByText("Event types")).toBeInTheDocument(); | ||
| expect(screen.getByText("When a customer accepts a quote")).toBeInTheDocument(); | ||
| expect(screen.getByRole("switch", { name: "Quotes accepted" })).toBeChecked(); | ||
| expect(screen.getByRole("switch", { name: "Low stock" })).not.toBeChecked(); | ||
| expect(screen.getByText("3 of 5 event types active")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("saves a per-event toggle and updates the summary", async () => { | ||
| renderPageWithI18n(<Settings />, await english()); | ||
| openTab("Notifications"); | ||
|
|
||
| fireEvent.click(await screen.findByRole("switch", { name: "Overdue invoices" })); | ||
|
|
||
| await waitFor(() => expect(http.put).toHaveBeenCalledWith("/notifications/preferences", { email_events: { InvoiceOverdue: true } })); | ||
| expect(await screen.findByText("4 of 5 event types active")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("turns everything off from the master switch", async () => { | ||
| renderPageWithI18n(<Settings />, await english()); | ||
| openTab("Notifications"); | ||
|
|
||
| fireEvent.click(await screen.findByRole("switch", { name: "Email notifications" })); | ||
|
|
||
| await waitFor(() => expect(http.put).toHaveBeenCalledWith("/notifications/preferences", { email_enabled: false })); | ||
| expect(await screen.findByText("All email notifications are turned off")).toBeInTheDocument(); | ||
| expect(screen.getByRole("switch", { name: "Quotes accepted" })).toBeDisabled(); | ||
| }); | ||
|
|
||
| it("explains the in-app notifications", async () => { | ||
| renderPageWithI18n(<Settings />, await english()); | ||
| openTab("Notifications"); | ||
|
|
||
| expect(await screen.findByText("Real-time notifications")).toBeInTheDocument(); | ||
| expect(screen.getByText(/delivered over WebSocket/)).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("general and team", () => { | ||
| it("shows the workspace form and the preferences", async () => { | ||
| renderPageWithI18n(<Settings />, await english()); | ||
| openTab("General"); | ||
|
|
||
| expect(await screen.findByText("Workspace settings")).toBeInTheDocument(); | ||
| expect(screen.getByLabelText("Workspace name")).toHaveValue("Composable OS"); | ||
| expect(screen.getByLabelText("Workspace URL")).toBeDisabled(); | ||
| expect(screen.getByRole("switch", { name: "Enable AI recommendations" })).toBeChecked(); | ||
| expect(screen.getByRole("switch", { name: "Automatic knowledge graph linking" })).toBeChecked(); | ||
| expect(screen.getByText("Preferences")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("lists the team with English roles", async () => { | ||
| renderPageWithI18n(<Settings />, await english()); | ||
| openTab("Team"); | ||
|
|
||
| expect(await screen.findByText("Team members")).toBeInTheDocument(); | ||
| expect(screen.getByRole("button", { name: /Invite/ })).toBeInTheDocument(); | ||
| expect(screen.getAllByText("Admin")).toHaveLength(2); | ||
| expect(screen.getAllByText("Member")).toHaveLength(4); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| { | ||
| "title": "Settings", | ||
| "subtitle": "Platform configuration, architecture and system status.", | ||
| "tabs": { | ||
| "architecture": "Architecture", | ||
| "health": "System health", | ||
| "notifications": "Notifications", | ||
| "general": "General", | ||
| "team": "Team" | ||
| }, | ||
| "architecture": { | ||
| "title": "Platform architecture", | ||
| "hint": "Hover over the components to follow the data flow paths.", | ||
| "layers": { | ||
| "presentation": "PRESENTATION", | ||
| "api": "API LAYER", | ||
| "microservices": "MICROSERVICES", | ||
| "messaging": "MESSAGING", | ||
| "data": "DATA LAYER" | ||
| }, | ||
| "nodes": { | ||
| "client": { "label": "Client apps", "sublabel": "React SPA / Mobile" }, | ||
| "gateway": { "label": "API Gateway", "sublabel": "Auth · Rate Limit · Routing" }, | ||
| "projects": { "label": "Projects", "sublabel": "Microservice" }, | ||
| "crm": { "label": "CRM", "sublabel": "Microservice" }, | ||
| "docs": { "label": "Documents", "sublabel": "Microservice" }, | ||
| "knowledge": { "label": "Knowledge", "sublabel": "Microservice" }, | ||
| "analytics": { "label": "Analytics", "sublabel": "Microservice" }, | ||
| "ai": { "label": "AI agents", "sublabel": "Microservice" }, | ||
| "eventbus": { "label": "Event Bus", "sublabel": "Async messaging · Pub/Sub" }, | ||
| "postgres": { "label": "PostgreSQL", "sublabel": "Primary data store" }, | ||
| "graph": { "label": "Graph database", "sublabel": "Neo4j · Relationships" }, | ||
| "vector": { "label": "Vector database", "sublabel": "Embeddings · Semantic search" } | ||
| }, | ||
| "cards": { | ||
| "gateway": { | ||
| "title": "API Gateway", | ||
| "description": "Handles authentication, rate limiting, validation and smart routing to the microservices. Supports REST and GraphQL." | ||
| }, | ||
| "eventbus": { | ||
| "title": "Event Bus", | ||
| "description": "Asynchronous pub/sub messaging keeps the modules loosely coupled. When a deal is closed in the CRM, events create the project automatically." | ||
| }, | ||
| "data": { | ||
| "title": "Data layer", | ||
| "description": "PostgreSQL for structured data, Neo4j for the knowledge graph relationships and a vector database for semantic search and AI embeddings." | ||
| } | ||
| } | ||
| }, | ||
| "health": { | ||
| "loading": "Loading system status...", | ||
| "updated": "Updated {{when}}", | ||
| "refresh": "Refresh", | ||
| "error": "Could not connect to the system. Check that the backend is running.", | ||
| "latency": "Latency", | ||
| "overall": "Overall status:", | ||
| "version": "v{{version}}", | ||
| "services": { | ||
| "api": "API Gateway", | ||
| "postgres": "PostgreSQL" | ||
| }, | ||
| "status": { | ||
| "healthy": "healthy", | ||
| "degraded": "degraded", | ||
| "unhealthy": "unhealthy" | ||
| } | ||
| }, | ||
| "notifications": { | ||
| "title": "Email notifications", | ||
| "saving": "Saving...", | ||
| "intro": "Control which business events send email alerts. Changes are saved automatically.", | ||
| "loading": "Loading preferences...", | ||
| "master": "Email notifications", | ||
| "masterHint": "Master switch — turn it off to stop all email alerts", | ||
| "eventTypes": "Event types", | ||
| "summaryOn": "{{active}} of {{total}} event types active", | ||
| "summaryOff": "All email notifications are turned off", | ||
| "realtimeTitle": "Real-time notifications", | ||
| "realtimeBody": "In-app notifications are delivered over WebSocket and are always on while you are connected. They cover 13 event types, including workflow status, sales activity, inventory alerts, portal interactions and accounting updates. Use the bell icon in the header to see them.", | ||
| "events": { | ||
| "QuoteAccepted": { | ||
| "label": "Quotes accepted", | ||
| "desc": "When a customer accepts a quote" | ||
| }, | ||
| "SalesOrderCreated": { | ||
| "label": "Sales orders created", | ||
| "desc": "When a new sales order is generated" | ||
| }, | ||
| "WorkflowFailed": { | ||
| "label": "Failed workflows", | ||
| "desc": "When an automated workflow fails" | ||
| }, | ||
| "InventoryLowThresholdDetected": { | ||
| "label": "Low stock", | ||
| "desc": "When a product reaches its minimum inventory threshold" | ||
| }, | ||
| "InvoiceOverdue": { | ||
| "label": "Overdue invoices", | ||
| "desc": "When an invoice passes its due date without payment" | ||
| } | ||
| } | ||
| }, | ||
| "general": { | ||
| "title": "Workspace settings", | ||
| "name": "Workspace name", | ||
| "url": "Workspace URL", | ||
| "preferences": "Preferences", | ||
| "prefs": { | ||
| "ai": { | ||
| "label": "Enable AI recommendations", | ||
| "desc": "Show AI suggestions across all modules" | ||
| }, | ||
| "projects": { | ||
| "label": "Create projects automatically from deals", | ||
| "desc": "Automatically generate a project when a CRM deal is closed" | ||
| }, | ||
| "graph": { | ||
| "label": "Automatic knowledge graph linking", | ||
| "desc": "Automatically detect and link entities in documents" | ||
| } | ||
| } | ||
| }, | ||
| "team": { | ||
| "title": "Team members", | ||
| "invite": "Invite", | ||
| "role": { | ||
| "admin": "Admin", | ||
| "member": "Member" | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this page uses the real
/healthresponse,backend/app/health.pyalways includes a third check namedredis(lines 86–89), but the new English service map only coversapiandpostgres.useEnumLabeltherefore falls back to the wire identifier and renders lowercaseredis, even though this namespace is now marked fully translated; add the Redis label to both base and English catalogues and cover the actual three-service response in the English test.Useful? React with 👍 / 👎.