fix(frontend): align Settings desktop labels on French (#78)

UX decision (carnet #78): the human UI of IdeA is French by default,
uniform per surface — proper nouns and technical acronyms (URL, LAN,
IP/CIDR, HTTPS, API, CLI…) stay as-is. Settings mixed English (AI
Profiles, Deployment) with French (Appareils, frozen by UX for #77).

Renames the Settings menu/nav, the Profils IA and Déploiement panels
(titles, actions, states, help text) to French, per the carnet's
exhaustive list. Updates the affected tests accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 14:53:32 +02:00
parent a69c5db093
commit 6e92536d84
6 changed files with 94 additions and 90 deletions

View File

@ -6,6 +6,9 @@
* is a radio choice with consequences, the authorized-proxy field is explicitly
* *not* a listen address, addresses come from the backend, a refusal is
* actionable, and no pairing code is ever shown here (#77).
*
* Labels asserted here are French (ticket #78 — UX decision: uniform French UI
* per surface, technical acronyms like URL/LAN/IP/CIDR/HTTPS excepted).
*/
import { describe, it, expect, vi } from "vitest";
@ -30,7 +33,7 @@ function renderView(desktopServer = new MockDesktopServerGateway()) {
/** Waits past the hook's preview debounce. */
async function settle() {
await screen.findByRole("radiogroup", { name: "exposure mode" });
await screen.findByRole("radiogroup", { name: "mode d'accès réseau" });
await waitFor(() => expect(screen.getByLabelText("Port")).toBeTruthy());
}
@ -43,58 +46,58 @@ describe("DeploymentSettings", () => {
renderView();
await settle();
const group = screen.getByRole("radiogroup", { name: "exposure mode" });
const group = screen.getByRole("radiogroup", { name: "mode d'accès réseau" });
expect(within(group).getAllByRole("radio")).toHaveLength(3);
// The plain-language consequence is part of the choice, not a tooltip.
expect(
within(group).getByText(/Remote devices cannot connect/),
within(group).getByText(/ne peuvent pas se connecter/),
).toBeTruthy();
expect(
within(group).getByText(/same machine as IdeA Desktop/),
within(group).getByText(/même machine qu'IdeA Desktop/),
).toBeTruthy();
expect(
within(group).getByText(/only accept traffic from that proxy/),
within(group).getByText(/n'acceptera que le trafic provenant de ce proxy/),
).toBeTruthy();
});
it("shows no exposure fields in 'This computer only'", async () => {
it("shows no exposure fields in 'Cet ordinateur uniquement'", async () => {
renderView();
await settle();
// Default is localOnly: nothing to configure, nothing to get wrong.
expect(screen.queryByLabelText("Public origin")).toBeNull();
expect(screen.queryByLabelText("Authorized proxy IP/CIDR")).toBeNull();
expect(screen.queryByLabelText("LAN address to bind")).toBeNull();
expect(screen.queryByLabelText("Origine publique")).toBeNull();
expect(screen.queryByLabelText("IP/CIDR du proxy autorisé")).toBeNull();
expect(screen.queryByLabelText("Adresse LAN d'écoute")).toBeNull();
});
it("asks only for a public origin when the proxy is on this computer", async () => {
renderView();
await settle();
selectMode("Remote access, proxy on this computer");
selectMode("Accès distant, proxy sur cet ordinateur");
expect(await screen.findByLabelText("Public origin")).toBeTruthy();
expect(await screen.findByLabelText("Origine publique")).toBeTruthy();
// The proxy is local, so there is nothing to authorize and nothing to bind.
expect(screen.queryByLabelText("Authorized proxy IP/CIDR")).toBeNull();
expect(screen.queryByLabelText("LAN address to bind")).toBeNull();
expect(screen.queryByLabelText("IP/CIDR du proxy autorisé")).toBeNull();
expect(screen.queryByLabelText("Adresse LAN d'écoute")).toBeNull();
});
it("explains that the authorized proxy is not a listen address, and warns about the mode", async () => {
renderView();
await settle();
selectMode("Remote access, proxy on another machine");
selectMode("Accès distant, proxy sur une autre machine");
// This help text is the whole point of the screen: it corrects the
// "that's where IdeA listens" misreading.
expect(
await screen.findByText(
"This is not where IdeA listens. It is the machine allowed to contact IdeA.",
"Ce n'est pas l'adresse d'écoute d'IdeA. C'est la machine autorisée à contacter IdeA.",
),
).toBeTruthy();
// The permanent warning about the silent-timeout failure mode.
expect(
screen.getByText(/the proxy may time out without an IdeA error/),
screen.getByText(/le proxy peut expirer sans erreur IdeA/),
).toBeTruthy();
});
@ -103,9 +106,9 @@ describe("DeploymentSettings", () => {
const preview = vi.spyOn(gateway, "previewExposure");
renderView(gateway);
await settle();
selectMode("Remote access, proxy on another machine");
selectMode("Accès distant, proxy sur une autre machine");
const select = await screen.findByLabelText("LAN address to bind");
const select = await screen.findByLabelText("Adresse LAN d'écoute");
const offered = within(select as HTMLElement)
.getAllByRole("option")
.map((o) => (o as HTMLOptionElement).value)
@ -120,7 +123,7 @@ describe("DeploymentSettings", () => {
renderView();
await settle();
// A remote mode with no origin: the backend says exactly what to fix.
selectMode("Remote access, proxy on this computer");
selectMode("Accès distant, proxy sur cet ordinateur");
const alert = await screen.findByRole("alert");
expect(alert.textContent).toMatch(/requires publicOrigin/);
@ -129,13 +132,13 @@ describe("DeploymentSettings", () => {
it("shows the upstream to paste, read-only, once the config is valid", async () => {
renderView();
await settle();
selectMode("Remote access, proxy on this computer");
fireEvent.change(await screen.findByLabelText("Public origin"), {
selectMode("Accès distant, proxy sur cet ordinateur");
fireEvent.change(await screen.findByLabelText("Origine publique"), {
target: { value: "https://idea.example.com" },
});
const upstream = await screen.findByRole("button", {
name: "copy upstream url",
name: "copier l'URL upstream",
});
expect(upstream).toBeTruthy();
// The upstream is IdeA-provided, never a field the user edits.
@ -152,8 +155,8 @@ describe("DeploymentSettings", () => {
expect(screen.queryByRole("button", { name: "copy pairing code" })).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Start" }));
await waitFor(() => expect(screen.getByText("Running")).toBeTruthy());
fireEvent.click(screen.getByRole("button", { name: "Démarrer" }));
await waitFor(() => expect(screen.getByText("En cours d'exécution")).toBeTruthy());
expect(screen.queryByRole("button", { name: "copy pairing code" })).toBeNull();
expect(screen.queryByText(/generate a pairing code/i)).toBeNull();
@ -165,17 +168,17 @@ describe("DeploymentSettings", () => {
renderView();
await settle();
const pairing = screen.getByText(/Pairing is managed in/);
expect(within(pairing).getByText("Settings → Appareils")).toBeTruthy();
const pairing = screen.getByText(/L'appairage est géré dans/);
expect(within(pairing).getByText("Paramètres → Appareils")).toBeTruthy();
});
it("starts the server and reports the local URL", async () => {
renderView();
await settle();
fireEvent.click(screen.getByRole("button", { name: "Start" }));
fireEvent.click(screen.getByRole("button", { name: "Démarrer" }));
await waitFor(() => expect(screen.getByText("Running")).toBeTruthy());
expect(screen.getByText(/Local URL:/)).toBeTruthy();
await waitFor(() => expect(screen.getByText("En cours d'exécution")).toBeTruthy());
expect(screen.getByText(/URL locale :/)).toBeTruthy();
});
it("persists the draft before starting, so what runs is what is shown", async () => {
@ -185,7 +188,7 @@ describe("DeploymentSettings", () => {
await settle();
fireEvent.change(screen.getByLabelText("Port"), { target: { value: "18080" } });
fireEvent.click(screen.getByRole("button", { name: "Start" }));
fireEvent.click(screen.getByRole("button", { name: "Démarrer" }));
await waitFor(() => expect(save).toHaveBeenCalled());
expect(save.mock.calls[0]![0]).toMatchObject({ port: 18080 });
@ -204,7 +207,7 @@ describe("DeploymentSettings", () => {
error: { code: "INVALID", message: "port 17373 already in use" },
});
await waitFor(() => expect(screen.getByText("Failed")).toBeTruthy());
await waitFor(() => expect(screen.getByText("Échec")).toBeTruthy());
expect(screen.getByRole("alert").textContent).toMatch(/already in use/);
});
});