- No bass line transcription yet. Use it when you want to check the groove before rehearsal.
+ {t("grooveEmptyHint")}
);
}
@@ -68,13 +70,13 @@ function GrooveMapComponent({ notes, isLoading }: GrooveMapProps) {
className="relative mt-4 overflow-x-auto rounded-lg border border-cyan-200/15 bg-slate-950/80 p-4 shadow-inner shadow-cyan-950/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
role="region"
tabIndex={0}
- aria-label="Bass transcription groove map"
+ aria-label={t("grooveMapAriaLabel")}
>
- Transcription complete. {renderedNotes.length} notes analyzed.
+ {t("grooveCompleteSrLabel").replace("{count}", String(renderedNotes.length))}
- {renderedNotes.length} notes mapped for rehearsal
+ {t("grooveNotesMappedLabel").replace("{count}", String(renderedNotes.length))}
diff --git a/apps/desktop/src/features/workspace/Workspace.test.tsx b/apps/desktop/src/features/workspace/Workspace.test.tsx
index a3da5ffe6..dfd6a4cd7 100644
--- a/apps/desktop/src/features/workspace/Workspace.test.tsx
+++ b/apps/desktop/src/features/workspace/Workspace.test.tsx
@@ -96,9 +96,29 @@ describe("Workspace", () => {
render(
);
fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" }));
- const transcribeButton = screen.getByRole("button", { name: "Transcribe Bass" }) as HTMLButtonElement;
+ const transcribeButton = screen.getByRole("button", { name: "Map bass notes" }) as HTMLButtonElement;
expect(transcribeButton.disabled).toBe(false);
- expect(transcribeButton.title).toBe("Transcribe part");
+ expect(transcribeButton.title).toBe("Show this part's bass notes on the groove map.");
+ });
+
+ it("labels unavailable transcription for the selected non-bass role instead of bass", () => {
+ setNavigatorLanguage("en-US");
+ const song = createDemoRehearsalSong();
+ song.sections[0]!.roles[0] = {
+ ...song.sections[0]!.roles[0]!,
+ id: "lead-vocal",
+ name: "Lead Vocal"
+ };
+
+ render(
);
+ fireEvent.click(screen.getByRole("tab", { name: "Lead Vocal" }));
+
+ const pendingButton = screen.getByRole("button", { name: "Lead Vocal · Coming soon" });
+ expect(pendingButton.textContent).toBe("Lead Vocal · Coming soon");
+ expect(pendingButton.textContent).not.toContain("Map bass notes");
+ expect(pendingButton.getAttribute("title")).toBe(
+ "Lead Vocal part is coming soon — bass is ready first."
+ );
});
it("renders bass transcription in the dark rehearsal cockpit system", () => {
@@ -115,7 +135,7 @@ describe("Workspace", () => {
render(
);
fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" }));
- const grooveMap = screen.getByRole("region", { name: /bass transcription groove map/i });
+ const grooveMap = screen.getByRole("region", { name: /bass groove map/i });
expect(grooveMap.className).toContain("bg-slate-950");
expect(screen.getByText("E2")).toBeTruthy();
expect(screen.getByText("G2")).toBeTruthy();
@@ -270,4 +290,4 @@ describe("Workspace", () => {
expect(screen.getByText("합주 우선순위")).toBeTruthy();
expect(screen.getByText("역할과 화성")).toBeTruthy();
});
-});
+});
\ No newline at end of file
diff --git a/apps/desktop/src/features/workspace/Workspace.timeline-summary.test.tsx b/apps/desktop/src/features/workspace/Workspace.timeline-summary.test.tsx
new file mode 100644
index 000000000..92c5fde84
--- /dev/null
+++ b/apps/desktop/src/features/workspace/Workspace.timeline-summary.test.tsx
@@ -0,0 +1,33 @@
+import { render, screen } from "@testing-library/react";
+import { createDemoRehearsalSong } from "@bandscope/shared-types";
+import { afterEach, describe, expect, it } from "vitest";
+import { Workspace } from "./Workspace";
+
+const originalLanguage = navigator.language;
+
+function setNavigatorLanguage(language: string) {
+ Object.defineProperty(navigator, "language", {
+ configurable: true,
+ value: language
+ });
+}
+
+describe("Workspace song timeline summary", () => {
+ afterEach(() => {
+ setNavigatorLanguage(originalLanguage);
+ });
+
+ it("uses cardinal-safe English copy for a one-section song", () => {
+ setNavigatorLanguage("en-US");
+ const song = createDemoRehearsalSong();
+ song.sections = song.sections.slice(0, 1);
+
+ render(
);
+
+ expect(
+ screen.getByText(
+ "Sections mapped: 1. Use the groove, role cues, and chord confidence notes to plan the first pass."
+ )
+ ).toBeTruthy();
+ });
+});
diff --git a/apps/desktop/src/features/workspace/Workspace.tsx b/apps/desktop/src/features/workspace/Workspace.tsx
index 71546b524..92dad17f4 100644
--- a/apps/desktop/src/features/workspace/Workspace.tsx
+++ b/apps/desktop/src/features/workspace/Workspace.tsx
@@ -83,7 +83,7 @@ const SongStructure = memo(function SongStructure({ sections, t }: { sections: R
role="region"
tabIndex={0}
className="overflow-x-auto rounded-2xl border border-white/10 bg-[linear-gradient(180deg,rgba(8,18,35,0.96),rgba(2,6,23,0.98))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
- aria-label="Scrollable song structure timeline"
+ aria-label={t("songStructureTimelineAriaLabel")}
>
- Export Cue Sheet (CSV)
+ {t("exportCueSheetButton")}
@@ -292,7 +292,7 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp