From 6e92536d8491b57fa3aaf7ed45fa92686024b170 Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 18 Jul 2026 14:53:32 +0200 Subject: [PATCH] fix(frontend): align Settings desktop labels on French (#78) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../features/first-run/ProfilesSettings.tsx | 10 +-- .../src/features/projects/ProjectsView.tsx | 5 +- .../src/features/projects/projects.test.tsx | 8 +- .../settings/DeploymentSettings.test.tsx | 65 ++++++++-------- .../features/settings/DeploymentSettings.tsx | 75 ++++++++++--------- .../src/features/settings/SettingsView.tsx | 21 +++--- 6 files changed, 94 insertions(+), 90 deletions(-) diff --git a/frontend/src/features/first-run/ProfilesSettings.tsx b/frontend/src/features/first-run/ProfilesSettings.tsx index 44b42e4..7a2d1d9 100644 --- a/frontend/src/features/first-run/ProfilesSettings.tsx +++ b/frontend/src/features/first-run/ProfilesSettings.tsx @@ -57,10 +57,10 @@ export function ProfilesSettings() { return ( setEditing(true)}> - Configure profiles + Configurer les profils } > @@ -72,7 +72,7 @@ export function ProfilesSettings() { )} {profiles.length === 0 ? ( -

No profiles configured.

+

Aucun profil configuré.

) : (
    {profiles.map((p) => ( @@ -87,10 +87,10 @@ export function ProfilesSettings() { ))} diff --git a/frontend/src/features/projects/ProjectsView.tsx b/frontend/src/features/projects/ProjectsView.tsx index 016f7a8..3af2ad4 100644 --- a/frontend/src/features/projects/ProjectsView.tsx +++ b/frontend/src/features/projects/ProjectsView.tsx @@ -401,9 +401,10 @@ export function ProjectsView() { }, { id: "settings", - label: "Settings", + label: "Paramètres", // One entry per section (#68). The entries name sections and mark the open - // one; closing lives in the view ("Close Settings"), so no label alternates. + // one; closing lives in the view ("Fermer les paramètres"), so no label + // alternates. items: SETTINGS_SECTIONS.map((section) => ({ id: section, label: SETTINGS_SECTION_LABEL[section], diff --git a/frontend/src/features/projects/projects.test.tsx b/frontend/src/features/projects/projects.test.tsx index 9dfa5ce..3d90f3c 100644 --- a/frontend/src/features/projects/projects.test.tsx +++ b/frontend/src/features/projects/projects.test.tsx @@ -208,13 +208,13 @@ describe("ProjectsView (with MockProjectGateway)", () => { expect(screen.queryByLabelText("ai profiles settings")).toBeNull(); // Settings → AI Profiles swaps the main area to the profiles settings. - openMenuItem("Settings", "AI Profiles"); + openMenuItem("Paramètres", "Profils IA"); expect(await screen.findByLabelText("ai profiles settings")).toBeTruthy(); expect(screen.queryByLabelText("project name")).toBeNull(); // #68: closing is an explicit action in the view, not an alternating menu // label — the label never scaled past one section. - fireEvent.click(screen.getByRole("button", { name: "Close Settings" })); + fireEvent.click(screen.getByRole("button", { name: "Fermer les paramètres" })); await waitFor(() => expect(screen.getByLabelText("project name")).toBeTruthy(), ); @@ -226,13 +226,13 @@ describe("ProjectsView (with MockProjectGateway)", () => { await waitForIdle(); // The Settings menu now names sections; Deployment opens directly. - openMenuItem("Settings", "Deployment"); + openMenuItem("Paramètres", "Déploiement"); expect(await screen.findByLabelText("deployment settings")).toBeTruthy(); expect(screen.queryByLabelText("ai profiles settings")).toBeNull(); // The internal nav column switches sections without leaving Settings. const nav = screen.getByRole("navigation", { name: "settings sections" }); - fireEvent.click(within(nav).getByRole("button", { name: "AI Profiles" })); + fireEvent.click(within(nav).getByRole("button", { name: "Profils IA" })); expect(await screen.findByLabelText("ai profiles settings")).toBeTruthy(); expect(screen.queryByLabelText("deployment settings")).toBeNull(); }); diff --git a/frontend/src/features/settings/DeploymentSettings.test.tsx b/frontend/src/features/settings/DeploymentSettings.test.tsx index a5fb94c..071fc9b 100644 --- a/frontend/src/features/settings/DeploymentSettings.test.tsx +++ b/frontend/src/features/settings/DeploymentSettings.test.tsx @@ -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/); }); }); diff --git a/frontend/src/features/settings/DeploymentSettings.tsx b/frontend/src/features/settings/DeploymentSettings.tsx index e85706f..71ff05a 100644 --- a/frontend/src/features/settings/DeploymentSettings.tsx +++ b/frontend/src/features/settings/DeploymentSettings.tsx @@ -39,30 +39,30 @@ interface ModeOption { const MODE_OPTIONS: ModeOption[] = [ { mode: "localOnly", - title: "This computer only", + title: "Cet ordinateur uniquement", description: - "For using IdeA on this desktop only. Remote devices cannot connect.", + "Pour utiliser IdeA uniquement sur cet ordinateur. Les appareils distants ne peuvent pas se connecter.", }, { mode: "remoteProxyLocal", - title: "Remote access, proxy on this computer", + title: "Accès distant, proxy sur cet ordinateur", description: - "Use this when your HTTPS proxy runs on the same machine as IdeA Desktop.", + "À utiliser quand le proxy HTTPS tourne sur la même machine qu'IdeA Desktop.", }, { mode: "remoteProxyOtherMachine", - title: "Remote access, proxy on another machine", + title: "Accès distant, proxy sur une autre machine", description: - "Use this when the HTTPS proxy runs on another machine. IdeA will only accept traffic from that proxy.", + "À utiliser quand le proxy HTTPS tourne sur une autre machine. IdeA n'acceptera que le trafic provenant de ce proxy.", }, ]; const STATE_LABEL: Record = { - stopped: "Stopped", - starting: "Starting…", - running: "Running", - stopping: "Stopping…", - failed: "Failed", + stopped: "Arrêté", + starting: "Démarrage…", + running: "En cours d'exécution", + stopping: "Arrêt…", + failed: "Échec", }; /** Copy-to-clipboard button; degrades to disabled where the API is absent. */ @@ -86,7 +86,7 @@ function CopyButton({ value, label }: { value: string; label: string }) { ); }} > - {copied ? "Copied" : "Copy"} + {copied ? "Copié" : "Copier"} ); } @@ -108,8 +108,8 @@ export function DeploymentSettings() { if (!vm.ready || !vm.settings) { return ( - -

    Loading…

    + +

    Chargement…

    ); } @@ -124,7 +124,7 @@ export function DeploymentSettings() {
    {/* ── Status ────────────────────────────────────────────────────────── */} void vm.stop()} disabled={vm.busy || transitioning} > - Stop + Arrêter ) : ( ) } @@ -161,12 +161,12 @@ export function DeploymentSettings() { {status.localUrl && (

    - Local URL: {status.localUrl} + URL locale : {status.localUrl}

    )} {status.publicUrl && (

    - Public URL: {status.publicUrl} + URL publique : {status.publicUrl}

    )} @@ -184,12 +184,12 @@ export function DeploymentSettings() {
    - {/* ── Exposure ──────────────────────────────────────────────────────── */} - + {/* ── Exposure (ticket #78 — "Accès réseau" per UX) ────────────────── */} +
    {MODE_OPTIONS.map((option) => { @@ -234,7 +234,7 @@ export function DeploymentSettings() { {remote && ( - + {({ id, describedBy }) => ( {/* Addresses come from the backend probe — never invented here. */} {({ id, describedBy }) => vm.candidateLanAddresses.length > 0 ? ( @@ -263,7 +263,7 @@ export function DeploymentSettings() { value={settings.lanBindAddress ?? ""} onChange={(e) => vm.setLanBindAddress(e.target.value)} > - + {vm.candidateLanAddresses.map((address) => ( {({ id, describedBy }) => (

    - If your proxy is not on this computer, choose this mode. - Otherwise the proxy may time out without an IdeA error. + Si votre proxy n'est pas sur cet ordinateur, choisissez ce mode. + Sinon, le proxy peut expirer sans erreur IdeA.

    )} @@ -321,16 +321,16 @@ export function DeploymentSettings() { {/* ── Proxy setup: the upstream to paste, backend-provided ──────────── */} {remote && ( - +

    - Point your HTTPS reverse proxy at this upstream. + Faites pointer votre proxy inverse HTTPS vers cet upstream.

    {vm.upstreamUrl ? ( - + ) : (

    - Complete the settings above to get the upstream URL. + Complétez les paramètres ci-dessus pour obtenir l'URL upstream.

    )}
    @@ -338,10 +338,11 @@ export function DeploymentSettings() { )} {/* ── Pairing moved to its own surface (#77) — leave a signpost ─────── */} - +

    - Pairing is managed in Settings → Appareils, - where you can generate a code and revoke devices. + L'appairage est géré dans{" "} + Paramètres → Appareils, où + vous pouvez générer un code et révoquer des appareils.

    diff --git a/frontend/src/features/settings/SettingsView.tsx b/frontend/src/features/settings/SettingsView.tsx index 645ae06..92cf453 100644 --- a/frontend/src/features/settings/SettingsView.tsx +++ b/frontend/src/features/settings/SettingsView.tsx @@ -13,6 +13,12 @@ * * `EmbedderSettings` / `ModelServersPanel` are **not** pulled in here — that is * a separate lateral rework. The section list is the seam they would slot into. + * + * Ticket #78 — UX decision: the human UI of IdeA is French by default, uniform + * per surface (proper nouns/technical acronyms like `URL`, `LAN`, `IP/CIDR`, + * `HTTPS`, `API`, `CLI` stay as-is). Settings previously mixed `AI Profiles` / + * `Deployment` (English) with `Appareils` (French, frozen by UX for #77); this + * aligns the whole surface on French. */ import { Button, cn } from "@/shared"; @@ -23,17 +29,10 @@ import { DeploymentSettings } from "./DeploymentSettings"; /** The Settings sections, in menu/nav order. */ export type SettingsSection = "aiProfiles" | "deployment" | "devices"; -/** - * Human labels, shared by the nav column and the `Settings` menu. - * - * `Appareils` is French where its neighbours are English: the device vocabulary - * is frozen by UX across web and desktop (carnet #77), and the web surface it - * mirrors is French throughout. Aligning the whole Settings surface on one - * language is a UX call beyond this ticket. - */ +/** Human labels, shared by the nav column and the `Settings` menu (ticket #78 — French). */ export const SETTINGS_SECTION_LABEL: Record = { - aiProfiles: "AI Profiles", - deployment: "Deployment", + aiProfiles: "Profils IA", + deployment: "Déploiement", devices: "Appareils", }; @@ -83,7 +82,7 @@ export function SettingsView({