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:
@ -57,10 +57,10 @@ export function ProfilesSettings() {
|
|||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
aria-label="ai profiles settings"
|
aria-label="ai profiles settings"
|
||||||
title="AI Profiles"
|
title="Profils IA"
|
||||||
actions={
|
actions={
|
||||||
<Button size="sm" onClick={() => setEditing(true)}>
|
<Button size="sm" onClick={() => setEditing(true)}>
|
||||||
Configure profiles
|
Configurer les profils
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -72,7 +72,7 @@ export function ProfilesSettings() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{profiles.length === 0 ? (
|
{profiles.length === 0 ? (
|
||||||
<p className="text-sm text-muted">No profiles configured.</p>
|
<p className="text-sm text-muted">Aucun profil configuré.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="flex flex-col divide-y divide-border">
|
<ul className="flex flex-col divide-y divide-border">
|
||||||
{profiles.map((p) => (
|
{profiles.map((p) => (
|
||||||
@ -87,10 +87,10 @@ export function ProfilesSettings() {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
aria-label={`delete ${p.name}`}
|
aria-label={`supprimer ${p.name}`}
|
||||||
onClick={() => void del(p.id)}
|
onClick={() => void del(p.id)}
|
||||||
>
|
>
|
||||||
Delete
|
Supprimer
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -401,9 +401,10 @@ export function ProjectsView() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "settings",
|
id: "settings",
|
||||||
label: "Settings",
|
label: "Paramètres",
|
||||||
// One entry per section (#68). The entries name sections and mark the open
|
// 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) => ({
|
items: SETTINGS_SECTIONS.map((section) => ({
|
||||||
id: section,
|
id: section,
|
||||||
label: SETTINGS_SECTION_LABEL[section],
|
label: SETTINGS_SECTION_LABEL[section],
|
||||||
|
|||||||
@ -208,13 +208,13 @@ describe("ProjectsView (with MockProjectGateway)", () => {
|
|||||||
expect(screen.queryByLabelText("ai profiles settings")).toBeNull();
|
expect(screen.queryByLabelText("ai profiles settings")).toBeNull();
|
||||||
|
|
||||||
// Settings → AI Profiles swaps the main area to the profiles settings.
|
// 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(await screen.findByLabelText("ai profiles settings")).toBeTruthy();
|
||||||
expect(screen.queryByLabelText("project name")).toBeNull();
|
expect(screen.queryByLabelText("project name")).toBeNull();
|
||||||
|
|
||||||
// #68: closing is an explicit action in the view, not an alternating menu
|
// #68: closing is an explicit action in the view, not an alternating menu
|
||||||
// label — the label never scaled past one section.
|
// 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(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByLabelText("project name")).toBeTruthy(),
|
expect(screen.getByLabelText("project name")).toBeTruthy(),
|
||||||
);
|
);
|
||||||
@ -226,13 +226,13 @@ describe("ProjectsView (with MockProjectGateway)", () => {
|
|||||||
await waitForIdle();
|
await waitForIdle();
|
||||||
|
|
||||||
// The Settings menu now names sections; Deployment opens directly.
|
// 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(await screen.findByLabelText("deployment settings")).toBeTruthy();
|
||||||
expect(screen.queryByLabelText("ai profiles settings")).toBeNull();
|
expect(screen.queryByLabelText("ai profiles settings")).toBeNull();
|
||||||
|
|
||||||
// The internal nav column switches sections without leaving Settings.
|
// The internal nav column switches sections without leaving Settings.
|
||||||
const nav = screen.getByRole("navigation", { name: "settings sections" });
|
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(await screen.findByLabelText("ai profiles settings")).toBeTruthy();
|
||||||
expect(screen.queryByLabelText("deployment settings")).toBeNull();
|
expect(screen.queryByLabelText("deployment settings")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,6 +6,9 @@
|
|||||||
* is a radio choice with consequences, the authorized-proxy field is explicitly
|
* is a radio choice with consequences, the authorized-proxy field is explicitly
|
||||||
* *not* a listen address, addresses come from the backend, a refusal is
|
* *not* a listen address, addresses come from the backend, a refusal is
|
||||||
* actionable, and no pairing code is ever shown here (#77).
|
* 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";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
@ -30,7 +33,7 @@ function renderView(desktopServer = new MockDesktopServerGateway()) {
|
|||||||
|
|
||||||
/** Waits past the hook's preview debounce. */
|
/** Waits past the hook's preview debounce. */
|
||||||
async function settle() {
|
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());
|
await waitFor(() => expect(screen.getByLabelText("Port")).toBeTruthy());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,58 +46,58 @@ describe("DeploymentSettings", () => {
|
|||||||
renderView();
|
renderView();
|
||||||
await settle();
|
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);
|
expect(within(group).getAllByRole("radio")).toHaveLength(3);
|
||||||
|
|
||||||
// The plain-language consequence is part of the choice, not a tooltip.
|
// The plain-language consequence is part of the choice, not a tooltip.
|
||||||
expect(
|
expect(
|
||||||
within(group).getByText(/Remote devices cannot connect/),
|
within(group).getByText(/ne peuvent pas se connecter/),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
within(group).getByText(/same machine as IdeA Desktop/),
|
within(group).getByText(/même machine qu'IdeA Desktop/),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
within(group).getByText(/only accept traffic from that proxy/),
|
within(group).getByText(/n'acceptera que le trafic provenant de ce proxy/),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows no exposure fields in 'This computer only'", async () => {
|
it("shows no exposure fields in 'Cet ordinateur uniquement'", async () => {
|
||||||
renderView();
|
renderView();
|
||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
// Default is localOnly: nothing to configure, nothing to get wrong.
|
// Default is localOnly: nothing to configure, nothing to get wrong.
|
||||||
expect(screen.queryByLabelText("Public origin")).toBeNull();
|
expect(screen.queryByLabelText("Origine publique")).toBeNull();
|
||||||
expect(screen.queryByLabelText("Authorized proxy IP/CIDR")).toBeNull();
|
expect(screen.queryByLabelText("IP/CIDR du proxy autorisé")).toBeNull();
|
||||||
expect(screen.queryByLabelText("LAN address to bind")).toBeNull();
|
expect(screen.queryByLabelText("Adresse LAN d'écoute")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("asks only for a public origin when the proxy is on this computer", async () => {
|
it("asks only for a public origin when the proxy is on this computer", async () => {
|
||||||
renderView();
|
renderView();
|
||||||
await settle();
|
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.
|
// 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("IP/CIDR du proxy autorisé")).toBeNull();
|
||||||
expect(screen.queryByLabelText("LAN address to bind")).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 () => {
|
it("explains that the authorized proxy is not a listen address, and warns about the mode", async () => {
|
||||||
renderView();
|
renderView();
|
||||||
await settle();
|
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
|
// This help text is the whole point of the screen: it corrects the
|
||||||
// "that's where IdeA listens" misreading.
|
// "that's where IdeA listens" misreading.
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText(
|
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();
|
).toBeTruthy();
|
||||||
|
|
||||||
// The permanent warning about the silent-timeout failure mode.
|
// The permanent warning about the silent-timeout failure mode.
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/the proxy may time out without an IdeA error/),
|
screen.getByText(/le proxy peut expirer sans erreur IdeA/),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -103,9 +106,9 @@ describe("DeploymentSettings", () => {
|
|||||||
const preview = vi.spyOn(gateway, "previewExposure");
|
const preview = vi.spyOn(gateway, "previewExposure");
|
||||||
renderView(gateway);
|
renderView(gateway);
|
||||||
await settle();
|
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)
|
const offered = within(select as HTMLElement)
|
||||||
.getAllByRole("option")
|
.getAllByRole("option")
|
||||||
.map((o) => (o as HTMLOptionElement).value)
|
.map((o) => (o as HTMLOptionElement).value)
|
||||||
@ -120,7 +123,7 @@ describe("DeploymentSettings", () => {
|
|||||||
renderView();
|
renderView();
|
||||||
await settle();
|
await settle();
|
||||||
// A remote mode with no origin: the backend says exactly what to fix.
|
// 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");
|
const alert = await screen.findByRole("alert");
|
||||||
expect(alert.textContent).toMatch(/requires publicOrigin/);
|
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 () => {
|
it("shows the upstream to paste, read-only, once the config is valid", async () => {
|
||||||
renderView();
|
renderView();
|
||||||
await settle();
|
await settle();
|
||||||
selectMode("Remote access, proxy on this computer");
|
selectMode("Accès distant, proxy sur cet ordinateur");
|
||||||
fireEvent.change(await screen.findByLabelText("Public origin"), {
|
fireEvent.change(await screen.findByLabelText("Origine publique"), {
|
||||||
target: { value: "https://idea.example.com" },
|
target: { value: "https://idea.example.com" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const upstream = await screen.findByRole("button", {
|
const upstream = await screen.findByRole("button", {
|
||||||
name: "copy upstream url",
|
name: "copier l'URL upstream",
|
||||||
});
|
});
|
||||||
expect(upstream).toBeTruthy();
|
expect(upstream).toBeTruthy();
|
||||||
// The upstream is IdeA-provided, never a field the user edits.
|
// 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();
|
expect(screen.queryByRole("button", { name: "copy pairing code" })).toBeNull();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Start" }));
|
fireEvent.click(screen.getByRole("button", { name: "Démarrer" }));
|
||||||
await waitFor(() => expect(screen.getByText("Running")).toBeTruthy());
|
await waitFor(() => expect(screen.getByText("En cours d'exécution")).toBeTruthy());
|
||||||
|
|
||||||
expect(screen.queryByRole("button", { name: "copy pairing code" })).toBeNull();
|
expect(screen.queryByRole("button", { name: "copy pairing code" })).toBeNull();
|
||||||
expect(screen.queryByText(/generate a pairing code/i)).toBeNull();
|
expect(screen.queryByText(/generate a pairing code/i)).toBeNull();
|
||||||
@ -165,17 +168,17 @@ describe("DeploymentSettings", () => {
|
|||||||
renderView();
|
renderView();
|
||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
const pairing = screen.getByText(/Pairing is managed in/);
|
const pairing = screen.getByText(/L'appairage est géré dans/);
|
||||||
expect(within(pairing).getByText("Settings → Appareils")).toBeTruthy();
|
expect(within(pairing).getByText("Paramètres → Appareils")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts the server and reports the local URL", async () => {
|
it("starts the server and reports the local URL", async () => {
|
||||||
renderView();
|
renderView();
|
||||||
await settle();
|
await settle();
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Start" }));
|
fireEvent.click(screen.getByRole("button", { name: "Démarrer" }));
|
||||||
|
|
||||||
await waitFor(() => expect(screen.getByText("Running")).toBeTruthy());
|
await waitFor(() => expect(screen.getByText("En cours d'exécution")).toBeTruthy());
|
||||||
expect(screen.getByText(/Local URL:/)).toBeTruthy();
|
expect(screen.getByText(/URL locale :/)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("persists the draft before starting, so what runs is what is shown", async () => {
|
it("persists the draft before starting, so what runs is what is shown", async () => {
|
||||||
@ -185,7 +188,7 @@ describe("DeploymentSettings", () => {
|
|||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
fireEvent.change(screen.getByLabelText("Port"), { target: { value: "18080" } });
|
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());
|
await waitFor(() => expect(save).toHaveBeenCalled());
|
||||||
expect(save.mock.calls[0]![0]).toMatchObject({ port: 18080 });
|
expect(save.mock.calls[0]![0]).toMatchObject({ port: 18080 });
|
||||||
@ -204,7 +207,7 @@ describe("DeploymentSettings", () => {
|
|||||||
error: { code: "INVALID", message: "port 17373 already in use" },
|
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/);
|
expect(screen.getByRole("alert").textContent).toMatch(/already in use/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -39,30 +39,30 @@ interface ModeOption {
|
|||||||
const MODE_OPTIONS: ModeOption[] = [
|
const MODE_OPTIONS: ModeOption[] = [
|
||||||
{
|
{
|
||||||
mode: "localOnly",
|
mode: "localOnly",
|
||||||
title: "This computer only",
|
title: "Cet ordinateur uniquement",
|
||||||
description:
|
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",
|
mode: "remoteProxyLocal",
|
||||||
title: "Remote access, proxy on this computer",
|
title: "Accès distant, proxy sur cet ordinateur",
|
||||||
description:
|
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",
|
mode: "remoteProxyOtherMachine",
|
||||||
title: "Remote access, proxy on another machine",
|
title: "Accès distant, proxy sur une autre machine",
|
||||||
description:
|
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<string, string> = {
|
const STATE_LABEL: Record<string, string> = {
|
||||||
stopped: "Stopped",
|
stopped: "Arrêté",
|
||||||
starting: "Starting…",
|
starting: "Démarrage…",
|
||||||
running: "Running",
|
running: "En cours d'exécution",
|
||||||
stopping: "Stopping…",
|
stopping: "Arrêt…",
|
||||||
failed: "Failed",
|
failed: "Échec",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Copy-to-clipboard button; degrades to disabled where the API is absent. */
|
/** 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"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -108,8 +108,8 @@ export function DeploymentSettings() {
|
|||||||
|
|
||||||
if (!vm.ready || !vm.settings) {
|
if (!vm.ready || !vm.settings) {
|
||||||
return (
|
return (
|
||||||
<Panel aria-label="deployment settings" title="Deployment">
|
<Panel aria-label="deployment settings" title="Déploiement">
|
||||||
<p className="text-sm text-muted">Loading…</p>
|
<p className="text-sm text-muted">Chargement…</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -124,7 +124,7 @@ export function DeploymentSettings() {
|
|||||||
<div aria-label="deployment settings" className="flex flex-col gap-4">
|
<div aria-label="deployment settings" className="flex flex-col gap-4">
|
||||||
{/* ── Status ────────────────────────────────────────────────────────── */}
|
{/* ── Status ────────────────────────────────────────────────────────── */}
|
||||||
<Panel
|
<Panel
|
||||||
title="Server"
|
title="Serveur"
|
||||||
actions={
|
actions={
|
||||||
running || status.state === "stopping" ? (
|
running || status.state === "stopping" ? (
|
||||||
<Button
|
<Button
|
||||||
@ -132,7 +132,7 @@ export function DeploymentSettings() {
|
|||||||
onClick={() => void vm.stop()}
|
onClick={() => void vm.stop()}
|
||||||
disabled={vm.busy || transitioning}
|
disabled={vm.busy || transitioning}
|
||||||
>
|
>
|
||||||
Stop
|
Arrêter
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@ -140,7 +140,7 @@ export function DeploymentSettings() {
|
|||||||
onClick={() => void vm.start()}
|
onClick={() => void vm.start()}
|
||||||
disabled={vm.busy || transitioning}
|
disabled={vm.busy || transitioning}
|
||||||
>
|
>
|
||||||
Start
|
Démarrer
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -161,12 +161,12 @@ export function DeploymentSettings() {
|
|||||||
|
|
||||||
{status.localUrl && (
|
{status.localUrl && (
|
||||||
<p className="text-xs text-muted">
|
<p className="text-xs text-muted">
|
||||||
Local URL: <code className="font-mono text-content">{status.localUrl}</code>
|
URL locale : <code className="font-mono text-content">{status.localUrl}</code>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{status.publicUrl && (
|
{status.publicUrl && (
|
||||||
<p className="text-xs text-muted">
|
<p className="text-xs text-muted">
|
||||||
Public URL: <code className="font-mono text-content">{status.publicUrl}</code>
|
URL publique : <code className="font-mono text-content">{status.publicUrl}</code>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -184,12 +184,12 @@ export function DeploymentSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
{/* ── Exposure ──────────────────────────────────────────────────────── */}
|
{/* ── Exposure (ticket #78 — "Accès réseau" per UX) ────────────────── */}
|
||||||
<Panel title="Exposure">
|
<Panel title="Accès réseau">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
aria-label="exposure mode"
|
aria-label="mode d'accès réseau"
|
||||||
className="flex flex-col gap-2"
|
className="flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
{MODE_OPTIONS.map((option) => {
|
{MODE_OPTIONS.map((option) => {
|
||||||
@ -234,7 +234,7 @@ export function DeploymentSettings() {
|
|||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{remote && (
|
{remote && (
|
||||||
<Field label="Public origin" hint="Example: https://idea.example.com">
|
<Field label="Origine publique" hint="Exemple : https://idea.example.com">
|
||||||
{({ id, describedBy }) => (
|
{({ id, describedBy }) => (
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
@ -251,8 +251,8 @@ export function DeploymentSettings() {
|
|||||||
<>
|
<>
|
||||||
{/* Addresses come from the backend probe — never invented here. */}
|
{/* Addresses come from the backend probe — never invented here. */}
|
||||||
<Field
|
<Field
|
||||||
label="LAN address to bind"
|
label="Adresse LAN d'écoute"
|
||||||
hint="The address on this machine that the proxy will connect to."
|
hint="L'adresse de cette machine à laquelle le proxy se connectera."
|
||||||
>
|
>
|
||||||
{({ id, describedBy }) =>
|
{({ id, describedBy }) =>
|
||||||
vm.candidateLanAddresses.length > 0 ? (
|
vm.candidateLanAddresses.length > 0 ? (
|
||||||
@ -263,7 +263,7 @@ export function DeploymentSettings() {
|
|||||||
value={settings.lanBindAddress ?? ""}
|
value={settings.lanBindAddress ?? ""}
|
||||||
onChange={(e) => vm.setLanBindAddress(e.target.value)}
|
onChange={(e) => vm.setLanBindAddress(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">Select an address…</option>
|
<option value="">Sélectionner une adresse…</option>
|
||||||
{vm.candidateLanAddresses.map((address) => (
|
{vm.candidateLanAddresses.map((address) => (
|
||||||
<option key={address} value={address}>
|
<option key={address} value={address}>
|
||||||
{address}
|
{address}
|
||||||
@ -283,8 +283,8 @@ export function DeploymentSettings() {
|
|||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label="Authorized proxy IP/CIDR"
|
label="IP/CIDR du proxy autorisé"
|
||||||
hint="This is not where IdeA listens. It is the machine allowed to contact IdeA."
|
hint="Ce n'est pas l'adresse d'écoute d'IdeA. C'est la machine autorisée à contacter IdeA."
|
||||||
>
|
>
|
||||||
{({ id, describedBy }) => (
|
{({ id, describedBy }) => (
|
||||||
<Input
|
<Input
|
||||||
@ -298,8 +298,8 @@ export function DeploymentSettings() {
|
|||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<p className="rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning">
|
<p className="rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||||
If your proxy is not on this computer, choose this mode.
|
Si votre proxy n'est pas sur cet ordinateur, choisissez ce mode.
|
||||||
Otherwise the proxy may time out without an IdeA error.
|
Sinon, le proxy peut expirer sans erreur IdeA.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -321,16 +321,16 @@ export function DeploymentSettings() {
|
|||||||
|
|
||||||
{/* ── Proxy setup: the upstream to paste, backend-provided ──────────── */}
|
{/* ── Proxy setup: the upstream to paste, backend-provided ──────────── */}
|
||||||
{remote && (
|
{remote && (
|
||||||
<Panel title="Proxy setup">
|
<Panel title="Configuration du proxy">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<p className="text-xs text-muted">
|
<p className="text-xs text-muted">
|
||||||
Point your HTTPS reverse proxy at this upstream.
|
Faites pointer votre proxy inverse HTTPS vers cet upstream.
|
||||||
</p>
|
</p>
|
||||||
{vm.upstreamUrl ? (
|
{vm.upstreamUrl ? (
|
||||||
<ReadOnlyValue value={vm.upstreamUrl} copyLabel="copy upstream url" />
|
<ReadOnlyValue value={vm.upstreamUrl} copyLabel="copier l'URL upstream" />
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-faint">
|
<p className="text-xs text-faint">
|
||||||
Complete the settings above to get the upstream URL.
|
Complétez les paramètres ci-dessus pour obtenir l'URL upstream.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -338,10 +338,11 @@ export function DeploymentSettings() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Pairing moved to its own surface (#77) — leave a signpost ─────── */}
|
{/* ── Pairing moved to its own surface (#77) — leave a signpost ─────── */}
|
||||||
<Panel title="Pairing">
|
<Panel title="Appairage">
|
||||||
<p className="text-sm text-muted">
|
<p className="text-sm text-muted">
|
||||||
Pairing is managed in <span className="text-content">Settings → Appareils</span>,
|
L'appairage est géré dans{" "}
|
||||||
where you can generate a code and revoke devices.
|
<span className="text-content">Paramètres → Appareils</span>, où
|
||||||
|
vous pouvez générer un code et révoquer des appareils.
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,6 +13,12 @@
|
|||||||
*
|
*
|
||||||
* `EmbedderSettings` / `ModelServersPanel` are **not** pulled in here — that is
|
* `EmbedderSettings` / `ModelServersPanel` are **not** pulled in here — that is
|
||||||
* a separate lateral rework. The section list is the seam they would slot into.
|
* 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";
|
import { Button, cn } from "@/shared";
|
||||||
@ -23,17 +29,10 @@ import { DeploymentSettings } from "./DeploymentSettings";
|
|||||||
/** The Settings sections, in menu/nav order. */
|
/** The Settings sections, in menu/nav order. */
|
||||||
export type SettingsSection = "aiProfiles" | "deployment" | "devices";
|
export type SettingsSection = "aiProfiles" | "deployment" | "devices";
|
||||||
|
|
||||||
/**
|
/** Human labels, shared by the nav column and the `Settings` menu (ticket #78 — French). */
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
export const SETTINGS_SECTION_LABEL: Record<SettingsSection, string> = {
|
export const SETTINGS_SECTION_LABEL: Record<SettingsSection, string> = {
|
||||||
aiProfiles: "AI Profiles",
|
aiProfiles: "Profils IA",
|
||||||
deployment: "Deployment",
|
deployment: "Déploiement",
|
||||||
devices: "Appareils",
|
devices: "Appareils",
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -83,7 +82,7 @@ export function SettingsView({
|
|||||||
|
|
||||||
<div className="mt-auto pt-2">
|
<div className="mt-auto pt-2">
|
||||||
<Button size="sm" variant="ghost" className="w-full" onClick={onClose}>
|
<Button size="sm" variant="ghost" className="w-full" onClick={onClose}>
|
||||||
Close Settings
|
Fermer les paramètres
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Reference in New Issue
Block a user