Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2e94e00
feat: 한국어 번역 추가 및 하드코딩된 텍스트 i18n 적용
seonghobae Aug 3, 2026
160ef9f
feat: 한국어 번역 추가 및 하드코딩된 텍스트 i18n 적용\n\n- Workspace 컴포넌트 하드코딩 텍스트 i18n…
seonghobae Aug 3, 2026
865112c
fix(i18n): prepare reviewed workspace localization cleanup
seonghobae Aug 4, 2026
364d5d8
ci(i18n): verify and publish workspace localization cleanup
seonghobae Aug 4, 2026
b54b879
fix(i18n): finalize safe Workspace localization
github-actions[bot] Aug 4, 2026
57ee0c4
fix(deps): prepare bounded undici security update
seonghobae Aug 4, 2026
3d64e8b
ci(deps): verify minimal undici security update
seonghobae Aug 4, 2026
3d041af
fix(deps): update Undici to 7.29.0
github-actions[bot] Aug 4, 2026
671d85c
docs(changelog): record Workspace localization and Undici fix
seonghobae Aug 4, 2026
ffc0972
fix(i18n): prepare reviewed Workspace corrections
seonghobae Aug 4, 2026
1a7aa00
ci(i18n): verify remaining Workspace review fixes
seonghobae Aug 4, 2026
87b3455
fix(i18n): address Workspace localization review
github-actions[bot] Aug 4, 2026
67d1136
feat: Workspace 컴포넌트 하드코딩 텍스트 i18n 적용\n\n- Workspace 컴포넌트 하드코딩 텍스트 i1…
seonghobae Aug 4, 2026
213ae6a
feat: Workspace 컴포넌트 하드코딩 텍스트 i18n 적용\n\n- Workspace 컴포넌트 하드코딩 텍스트 i1…
seonghobae Aug 4, 2026
9d6e28e
ci: verify and repair PR 744 canonical npm lock
seonghobae Aug 5, 2026
e19f727
fix(deps): restore canonical hash-bounded npm lock
github-actions[bot] Aug 5, 2026
54577e1
docs: CHANGELOG 항목 한국어로 번역
seonghobae Aug 5, 2026
1c16b5c
fix: revert Undici update and nested lock drift
seonghobae Aug 7, 2026
4c09b53
fix(deps): update pdfjs-dist to 6.2.108 for CVE-2026-16633
seonghobae Aug 7, 2026
490f675
fix(security): require patched PDF.js
seonghobae Aug 11, 2026
23b9fd1
fix(security): refresh vulnerable npm transitive pins
seonghobae Aug 11, 2026
7ab1ea1
fix(deps): restore manifest-lock consistency
seonghobae Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

### Changed

- Localize Workspace export, stem-player, loop, solo, mute, and transcription controls in English and Korean, including safe literal placeholder interpolation and an accessible localized role fallback.

### Fixed

- Update Undici to 7.29.0 in the root npm lock to remediate the current high-severity advisory set without introducing nested workspace-lock drift.

## [0.1.3] - 2026-04-29

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.24.0",
"pdfjs-dist": "6.1.200",
"pdfjs-dist": "^6.2.108",
"react": "^19.2.4",
"react-dom": "^19.2.7",
"sonner": "^2.0.7",
Expand Down
22 changes: 21 additions & 1 deletion apps/desktop/src/features/workspace/Workspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ describe("Workspace", () => {
});

it("enables bass transcription from selected role metadata rather than role id text", () => {
setNavigatorLanguage("en-US");
const song = createDemoRehearsalSong();
song.sections[0]!.roles[0] = {
...song.sections[0]!.roles[0]!,
Expand All @@ -96,11 +97,30 @@ describe("Workspace", () => {
render(<Workspace song={song} />);
fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" }));

const transcribeButton = screen.getByRole("button", { name: "Transcribe Bass" }) as HTMLButtonElement;
const transcribeButton = screen.getByRole("button", { name: /Transcribe Bass/ }) as HTMLButtonElement;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(transcribeButton.disabled).toBe(false);
expect(transcribeButton.title).toBe("Transcribe part");
});

it("names unavailable transcription for the selected non-bass role", () => {
setNavigatorLanguage("en-US");
const song = createDemoRehearsalSong();
song.sections[0]!.roles[0] = {
...song.sections[0]!.roles[0]!,
id: "guitar-role",
name: "Guitar"
};

render(<Workspace song={song} />);
fireEvent.click(screen.getByRole("tab", { name: "Guitar" }));

const label = "Guitar transcription is coming soon. Bass is ready first.";
const transcribeButton = screen.getByRole("button", { name: label });
expect(transcribeButton).toHaveTextContent("Transcribe part");
expect(transcribeButton).toHaveAttribute("aria-disabled", "true");
expect(transcribeButton).toHaveAttribute("title", label);
});

it("renders bass transcription in the dark rehearsal cockpit system", () => {
const song = createDemoRehearsalSong();
song.sections[0]!.roles[0] = {
Expand Down
38 changes: 21 additions & 17 deletions apps/desktop/src/features/workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
return roleMap.get(activeRole);
}, [activeRole, roleMap]);
const canTranscribeBass = activeRoleDetails?.name.toLowerCase().includes("bass") ?? false;
const transcriptionUnavailableLabel = t("transcriptionComingSoon", {
roleName: activeRoleDetails?.name ?? t("thisRole")
});

/** Handle the practice progress change internally by immutably updating the song state. */
const handlePracticeProgressChange = (newProgress: number) => {
Expand Down Expand Up @@ -263,7 +266,7 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
className="min-h-10 border-cyan-300/30 bg-cyan-300/10 font-semibold text-cyan-50 shadow-[0_10px_30px_rgba(34,211,238,0.16)] hover:bg-cyan-300/20 hover:text-white"
>
<Download className="mr-2 size-4 text-cyan-200" aria-hidden="true" />
Export Cue Sheet (CSV)
{t("exportCueSheetCsv")}
</Button>
<Button
variant="outline"
Expand All @@ -272,7 +275,7 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
className="min-h-10 border-white/10 bg-white/5 font-semibold text-slate-100 shadow-sm hover:bg-white/10 hover:text-white"
>
<Download className="mr-2 size-4 text-slate-300" aria-hidden="true" />
Export Chart (JSON)
{t("exportChartJson")}
</Button>
<Button
variant="outline"
Expand All @@ -281,7 +284,7 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
className="min-h-10 border-teal-300/25 bg-teal-300/10 font-semibold text-teal-50 shadow-sm hover:bg-teal-300/20 hover:text-white"
>
<Download className="mr-2 size-4 text-teal-200" aria-hidden="true" />
Export Handoff (JSON)
{t("exportHandoffJson")}
</Button>
</div>
</div>
Expand Down Expand Up @@ -348,61 +351,62 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp

{activeRole && (
<div className="mb-4 rounded-2xl border border-emerald-300/20 bg-emerald-300/[0.06] p-4">
<p className="text-xs font-black uppercase tracking-[0.24em] text-emerald-200">Stem Player</p>
<p className="text-xs font-black uppercase tracking-[0.24em] text-emerald-200">{t("stemPlayerLabel")}</p>
<p className="mt-1 text-sm font-semibold text-slate-100">{activeRoleDetails?.name ?? activeRole}</p>
<div className="mt-3 flex flex-wrap gap-2">
<Button
type="button"
aria-disabled={true}
aria-label="Play stem coming soon"
title="Play stem coming soon"
aria-label={t("playStemSoon")}
title={t("playStemSoon")}
onClick={preventUnavailableAction}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 text-slate-400 opacity-70"
>
Play stem
{t("playStem")}
</Button>
<Button
type="button"
aria-disabled={true}
aria-label="Loop section coming soon"
title="Loop section coming soon"
aria-label={t("loopSectionSoon")}
title={t("loopSectionSoon")}
onClick={preventUnavailableAction}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 text-slate-400 opacity-70"
>
Loop section
{t("loopSection")}
</Button>
<Button
type="button"
aria-disabled={true}
aria-label="Solo / mute others coming soon"
title="Solo / mute others coming soon"
aria-label={t("soloMuteOthersSoon")}
title={t("soloMuteOthersSoon")}
onClick={preventUnavailableAction}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 text-slate-400 opacity-70"
>
Solo / mute others
{t("soloMuteOthers")}
</Button>
{canTranscribeBass ? (
<Button
type="button"
title="Transcribe part"
title={t("transcribePart")}
variant="outline"
className="min-h-11 border-emerald-300/20 bg-emerald-300/10 font-semibold text-emerald-100 disabled:cursor-not-allowed disabled:border-white/10 disabled:bg-white/5 disabled:text-slate-500"
>
Transcribe Bass
{t("transcribeBass")}
</Button>
) : (
<Button
type="button"
aria-disabled={true}
title={`${activeRoleDetails?.name ?? "This role"} transcription is coming soon. Bass is ready first.`}
aria-label={transcriptionUnavailableLabel}
title={transcriptionUnavailableLabel}
onClick={preventUnavailableAction}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 font-semibold text-slate-500 opacity-70"
>
Transcribe Bass
{t("transcribePart")}
</Button>
)}
</div>
Expand Down
18 changes: 18 additions & 0 deletions apps/desktop/src/i18n/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,21 @@ describe("i18n", () => {
});
});
});

describe("translator placeholder interpolation", () => {
it("does not recursively interpolate placeholder text inside replacement values", () => {
const t = createTranslator("en");

expect(
t("transcriptionComingSoon", { roleName: "Bass {roleName}" })
).toBe("Bass {roleName} transcription is coming soon. Bass is ready first.");
});

it("preserves replacement characters literally", () => {
const t = createTranslator("en");

expect(t("transcriptionComingSoon", { roleName: "$& [lead].*" })).toBe(
"$& [lead].* transcription is coming soon. Bass is ready first."
);
});
});
15 changes: 12 additions & 3 deletions apps/desktop/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,19 @@ const dictionaries = {
ko: koCommon
} as const;

/** Documented. */
/** Create a locale-bound translator with literal placeholder interpolation. */
export function createTranslator(locale: Locale = "en") {
return function t(key: TranslationKey): string {
return dictionaries[locale][key] ?? dictionaries.en[key];
return function translate(
key: TranslationKey,
variables?: Readonly<Record<string, string>>
): string {
let text = dictionaries[locale][key] ?? dictionaries.en[key];
if (variables) {
for (const [variableName, variableValue] of Object.entries(variables)) {
text = text.split(`{${variableName}}`).join(variableValue);
}
}
return text;
};
}

Expand Down
16 changes: 15 additions & 1 deletion apps/desktop/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,19 @@
"practiceProgressRegionLabel": "Practice Progress",
"practiceProgressLabel": "Practice Progress",
"decreasePracticeProgressLabel": "Decrease progress",
"increasePracticeProgressLabel": "Increase progress"
"increasePracticeProgressLabel": "Increase progress",
"exportCueSheetCsv": "Export Cue Sheet (CSV)",
"exportChartJson": "Export Chart (JSON)",
"exportHandoffJson": "Export Handoff (JSON)",
"stemPlayerLabel": "Stem Player",
"playStem": "Play stem",
"playStemSoon": "Stem playback is coming soon.",
"loopSection": "Loop section",
"loopSectionSoon": "Section looping is coming soon.",
"soloMuteOthers": "Solo / mute others",
"soloMuteOthersSoon": "Solo and mute controls are coming soon.",
"transcribeBass": "Transcribe Bass",
"transcribePart": "Transcribe part",
"transcriptionComingSoon": "{roleName} transcription is coming soon. Bass is ready first.",
"thisRole": "This role"
}
16 changes: 15 additions & 1 deletion apps/desktop/src/locales/ko/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,19 @@
"practiceProgressRegionLabel": "연습 진척도",
"practiceProgressLabel": "연습 진척도",
"decreasePracticeProgressLabel": "진척도 감소",
"increasePracticeProgressLabel": "진척도 증가"
"increasePracticeProgressLabel": "진척도 증가",
"exportCueSheetCsv": "큐 시트 내보내기 (CSV)",
"exportChartJson": "차트 내보내기 (JSON)",
"exportHandoffJson": "핸드오프 내보내기 (JSON)",
"stemPlayerLabel": "스템 플레이어",
"playStem": "스템 재생",
"playStemSoon": "스템 재생은 곧 제공됩니다",
"loopSection": "구간 반복",
"loopSectionSoon": "구간 반복은 곧 제공됩니다",
"soloMuteOthers": "솔로 / 나머지 음소거",
"soloMuteOthersSoon": "솔로 / 나머지 음소거는 곧 제공됩니다",
"transcribeBass": "베이스 채보",
"transcribePart": "파트 채보",
"transcriptionComingSoon": "{roleName} 채보는 곧 제공됩니다. 베이스가 먼저 준비되었습니다.",
"thisRole": "이 역할"
}
Loading
Loading