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:
@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user