-
Stem Player
+
{t("stemPlayerLabel" as Extract)}
{activeRoleDetails?.name ?? activeRole}
{canTranscribeBass ? (
) : (
)}
diff --git a/apps/desktop/src/i18n/index.ts b/apps/desktop/src/i18n/index.ts
index 1a9f471f0..4de43a59c 100644
--- a/apps/desktop/src/i18n/index.ts
+++ b/apps/desktop/src/i18n/index.ts
@@ -13,8 +13,14 @@ const dictionaries = {
/** Documented. */
export function createTranslator(locale: Locale = "en") {
- return function t(key: TranslationKey): string {
- return dictionaries[locale][key] ?? dictionaries.en[key];
+ return function t(key: TranslationKey, variables?: Record
): string {
+ let text = dictionaries[locale][key] ?? dictionaries.en[key];
+ if (variables) {
+ for (const [k, v] of Object.entries(variables)) {
+ text = text.replace(new RegExp(`{${k}}`, "g"), v);
+ }
+ }
+ return text;
};
}
diff --git a/apps/desktop/src/locales/en/common.json b/apps/desktop/src/locales/en/common.json
index 39f716d50..7a1fdfd34 100644
--- a/apps/desktop/src/locales/en/common.json
+++ b/apps/desktop/src/locales/en/common.json
@@ -148,5 +148,18 @@
"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": "Play stem coming soon",
+ "loopSection": "Loop section",
+ "loopSectionSoon": "Loop section coming soon",
+ "soloMuteOthers": "Solo / mute others",
+ "soloMuteOthersSoon": "Solo / mute others coming soon",
+ "transcribeBass": "Transcribe Bass",
+ "transcribePart": "Transcribe part",
+ "transcriptionComingSoon": "{roleName} transcription is coming soon. Bass is ready first."
}
diff --git a/apps/desktop/src/locales/ko/common.json b/apps/desktop/src/locales/ko/common.json
index 371884abb..edbe2f9cc 100644
--- a/apps/desktop/src/locales/ko/common.json
+++ b/apps/desktop/src/locales/ko/common.json
@@ -148,5 +148,18 @@
"practiceProgressRegionLabel": "연습 진척도",
"practiceProgressLabel": "연습 진척도",
"decreasePracticeProgressLabel": "진척도 감소",
- "increasePracticeProgressLabel": "진척도 증가"
+ "increasePracticeProgressLabel": "진척도 증가",
+ "exportCueSheetCsv": "큐 시트 내보내기 (CSV)",
+ "exportChartJson": "차트 내보내기 (JSON)",
+ "exportHandoffJson": "핸드오프 내보내기 (JSON)",
+ "stemPlayerLabel": "스템 플레이어",
+ "playStem": "스템 재생",
+ "playStemSoon": "스템 재생은 곧 제공됩니다",
+ "loopSection": "구간 반복",
+ "loopSectionSoon": "구간 반복은 곧 제공됩니다",
+ "soloMuteOthers": "솔로 / 나머지 음소거",
+ "soloMuteOthersSoon": "솔로 / 나머지 음소거는 곧 제공됩니다",
+ "transcribeBass": "베이스 채보",
+ "transcribePart": "파트 채보",
+ "transcriptionComingSoon": "{roleName} 채보는 곧 제공됩니다. 베이스가 먼저 준비되었습니다."
}
diff --git a/package-lock.json b/package-lock.json
index cf1c991c1..2fc0f2024 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -955,7 +955,6 @@
"os": [
"aix"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -973,7 +972,6 @@
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -991,7 +989,6 @@
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1009,7 +1006,6 @@
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1027,7 +1023,6 @@
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1045,7 +1040,6 @@
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1063,7 +1057,6 @@
"os": [
"freebsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1081,7 +1074,6 @@
"os": [
"freebsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1099,7 +1091,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1117,7 +1108,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1135,7 +1125,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1153,7 +1142,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1171,7 +1159,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1189,7 +1176,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1207,7 +1193,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1225,7 +1210,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1243,7 +1227,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1261,7 +1244,6 @@
"os": [
"netbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1279,7 +1261,6 @@
"os": [
"netbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1297,7 +1278,6 @@
"os": [
"openbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1315,7 +1295,6 @@
"os": [
"openbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1333,7 +1312,6 @@
"os": [
"openharmony"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1351,7 +1329,6 @@
"os": [
"sunos"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1369,7 +1346,6 @@
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1387,7 +1363,6 @@
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1405,7 +1380,6 @@
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
From 160ef9f107325010736c66af5f76f53415b6f4f2 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Mon, 3 Aug 2026 22:44:01 +0000
Subject: [PATCH 02/22] =?UTF-8?q?feat:=20=ED=95=9C=EA=B5=AD=EC=96=B4=20?=
=?UTF-8?q?=EB=B2=88=EC=97=AD=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=95=98?=
=?UTF-8?q?=EB=93=9C=EC=BD=94=EB=94=A9=EB=90=9C=20=ED=85=8D=EC=8A=A4?=
=?UTF-8?q?=ED=8A=B8=20i18n=20=EC=A0=81=EC=9A=A9\n\n-=20Workspace=20?=
=?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=ED=95=98=EB=93=9C?=
=?UTF-8?q?=EC=BD=94=EB=94=A9=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20i18n=20?=
=?UTF-8?q?=EC=A0=81=EC=9A=A9\n-=20security=20audit=20fail(undici=20vulner?=
=?UTF-8?q?ability)=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=97=85=EB=8D=B0?=
=?UTF-8?q?=EC=9D=B4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 2fc0f2024..ff7604e43 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7153,9 +7153,9 @@
}
},
"node_modules/undici": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
- "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
"dev": true,
"license": "MIT",
"engines": {
From 865112c5b429fe381ac66d57450751d532ce9da1 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 4 Aug 2026 12:26:32 +0900
Subject: [PATCH 03/22] fix(i18n): prepare reviewed workspace localization
cleanup
---
scripts/ci/finalize_workspace_i18n.py | 134 ++++++++++++++++++++++++++
1 file changed, 134 insertions(+)
create mode 100644 scripts/ci/finalize_workspace_i18n.py
diff --git a/scripts/ci/finalize_workspace_i18n.py b/scripts/ci/finalize_workspace_i18n.py
new file mode 100644
index 000000000..b8812a642
--- /dev/null
+++ b/scripts/ci/finalize_workspace_i18n.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+"""Finalize Workspace localization safely, add regressions, and self-delete."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+WORKSPACE = ROOT / "apps/desktop/src/features/workspace/Workspace.tsx"
+I18N = ROOT / "apps/desktop/src/i18n/index.ts"
+I18N_TEST = ROOT / "apps/desktop/src/i18n/index.test.ts"
+EN = ROOT / "apps/desktop/src/locales/en/common.json"
+KO = ROOT / "apps/desktop/src/locales/ko/common.json"
+SELF = ROOT / "scripts/ci/finalize_workspace_i18n.py"
+WORKFLOW = ROOT / ".github/workflows/finalize-workspace-i18n.yml"
+
+
+def replace_once(text: str, old: str, new: str, label: str) -> str:
+ """Replace exactly one reviewed fragment and fail on branch drift."""
+ count = text.count(old)
+ if count != 1:
+ raise RuntimeError(f"{label}: expected one match, found {count}")
+ return text.replace(old, new, 1)
+
+
+def patch_workspace(text: str) -> str:
+ """Remove redundant type assertions and localize the fallback role name."""
+ assertion = ' as Extract'
+ assertion_count = text.count(assertion)
+ if assertion_count < 10:
+ raise RuntimeError(
+ f"workspace translation assertions drifted: expected at least 10, found {assertion_count}"
+ )
+ text = text.replace(assertion, "")
+ return replace_once(
+ text,
+ 'activeRoleDetails?.name ?? "This role"',
+ 'activeRoleDetails?.name ?? t("thisRole")',
+ "localized role fallback",
+ )
+
+
+def patch_i18n(text: str) -> str:
+ """Interpolate literal placeholders without regular-expression semantics."""
+ old = '''/** Documented. */
+export function createTranslator(locale: Locale = "en") {
+ return function t(key: TranslationKey, variables?: Record): string {
+ let text = dictionaries[locale][key] ?? dictionaries.en[key];
+ if (variables) {
+ for (const [k, v] of Object.entries(variables)) {
+ text = text.replace(new RegExp(`{${k}}`, "g"), v);
+ }
+ }
+ return text;
+ };
+}
+'''
+ new = '''/** Create a locale-bound translator with literal placeholder interpolation. */
+export function createTranslator(locale: Locale = "en") {
+ return function translate(
+ key: TranslationKey,
+ variables?: Readonly>
+ ): 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;
+ };
+}
+'''
+ return replace_once(text, old, new, "translator interpolation")
+
+
+def patch_locale(path: Path, key: str, value: str) -> None:
+ """Add one synchronized locale entry without changing existing translations."""
+ payload = json.loads(path.read_text(encoding="utf-8"))
+ if not isinstance(payload, dict):
+ raise RuntimeError(f"locale root must be an object: {path}")
+ existing = payload.get(key)
+ if existing not in {None, value}:
+ raise RuntimeError(f"locale key conflict for {key}: {path}")
+ payload[key] = value
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
+
+
+def patch_tests(text: str) -> str:
+ """Add regression coverage for repeated and literal replacement values."""
+ addition = '''
+
+describe("translator placeholder interpolation", () => {
+ it("replaces every occurrence of a named placeholder", () => {
+ 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."
+ );
+ });
+});
+'''
+ if 'describe("translator placeholder interpolation"' in text:
+ raise RuntimeError("translator interpolation regression already exists")
+ return text.rstrip() + addition
+
+
+def main() -> int:
+ """Apply reviewed localization changes and remove temporary automation files."""
+ WORKSPACE.write_text(
+ patch_workspace(WORKSPACE.read_text(encoding="utf-8")), encoding="utf-8"
+ )
+ I18N.write_text(patch_i18n(I18N.read_text(encoding="utf-8")), encoding="utf-8")
+ I18N_TEST.write_text(
+ patch_tests(I18N_TEST.read_text(encoding="utf-8")), encoding="utf-8"
+ )
+ patch_locale(EN, "thisRole", "This role")
+ patch_locale(KO, "thisRole", "이 역할")
+ SELF.unlink()
+ WORKFLOW.unlink()
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
From 364d5d8612f84732d49fb51c3197f3b1753e2a7d Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 4 Aug 2026 12:27:10 +0900
Subject: [PATCH 04/22] ci(i18n): verify and publish workspace localization
cleanup
---
.github/workflows/finalize-workspace-i18n.yml | 84 +++++++++++++++++++
1 file changed, 84 insertions(+)
create mode 100644 .github/workflows/finalize-workspace-i18n.yml
diff --git a/.github/workflows/finalize-workspace-i18n.yml b/.github/workflows/finalize-workspace-i18n.yml
new file mode 100644
index 000000000..6f779621d
--- /dev/null
+++ b/.github/workflows/finalize-workspace-i18n.yml
@@ -0,0 +1,84 @@
+name: Finalize Workspace localization
+
+on:
+ push:
+ branches: [feat/i18n-workspace-buttons-12938137214845538471]
+ paths:
+ - scripts/ci/finalize_workspace_i18n.py
+ - .github/workflows/finalize-workspace-i18n.yml
+ workflow_dispatch:
+
+concurrency:
+ group: finalize-workspace-i18n
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ finalize:
+ if: >-
+ github.repository == 'ContextualWisdomLab/bandscope'
+ && github.actor != 'github-actions[bot]'
+ && github.ref_name == 'feat/i18n-workspace-buttons-12938137214845538471'
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ permissions:
+ contents: write
+ steps:
+ - name: Harden runner
+ uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
+ with:
+ egress-policy: audit
+
+ - name: Checkout exact feature branch
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ ref: feat/i18n-workspace-buttons-12938137214845538471
+ fetch-depth: 0
+ persist-credentials: true
+
+ - name: Use repository Node.js contract
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: 22.22.3
+ cache: npm
+
+ - name: Apply reviewed localization cleanup
+ run: |
+ set -euo pipefail
+ python3 -m py_compile scripts/ci/finalize_workspace_i18n.py
+ python3 scripts/ci/finalize_workspace_i18n.py
+ git show acdbea6344fe1231c39535b575f4de35e4c607c9:package-lock.json > package-lock.json
+
+ - name: Verify localization and desktop quality gates
+ run: |
+ set -euo pipefail
+ git diff --check
+ python3 -m json.tool apps/desktop/src/locales/en/common.json >/dev/null
+ python3 -m json.tool apps/desktop/src/locales/ko/common.json >/dev/null
+ grep -F 'text.split(`{${variableName}}`).join(variableValue)' apps/desktop/src/i18n/index.ts >/dev/null
+ grep -F 't("thisRole")' apps/desktop/src/features/workspace/Workspace.tsx >/dev/null
+ ! grep -F 'Extract
Date: Tue, 4 Aug 2026 03:28:12 +0000
Subject: [PATCH 05/22] fix(i18n): finalize safe Workspace localization
---
.github/workflows/finalize-workspace-i18n.yml | 84 -----------
.../src/features/workspace/Workspace.tsx | 34 ++---
apps/desktop/src/i18n/index.test.ts | 18 +++
apps/desktop/src/i18n/index.ts | 11 +-
apps/desktop/src/locales/en/common.json | 3 +-
apps/desktop/src/locales/ko/common.json | 3 +-
package-lock.json | 32 ++++-
scripts/ci/finalize_workspace_i18n.py | 134 ------------------
8 files changed, 75 insertions(+), 244 deletions(-)
delete mode 100644 .github/workflows/finalize-workspace-i18n.yml
delete mode 100644 scripts/ci/finalize_workspace_i18n.py
diff --git a/.github/workflows/finalize-workspace-i18n.yml b/.github/workflows/finalize-workspace-i18n.yml
deleted file mode 100644
index 6f779621d..000000000
--- a/.github/workflows/finalize-workspace-i18n.yml
+++ /dev/null
@@ -1,84 +0,0 @@
-name: Finalize Workspace localization
-
-on:
- push:
- branches: [feat/i18n-workspace-buttons-12938137214845538471]
- paths:
- - scripts/ci/finalize_workspace_i18n.py
- - .github/workflows/finalize-workspace-i18n.yml
- workflow_dispatch:
-
-concurrency:
- group: finalize-workspace-i18n
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- finalize:
- if: >-
- github.repository == 'ContextualWisdomLab/bandscope'
- && github.actor != 'github-actions[bot]'
- && github.ref_name == 'feat/i18n-workspace-buttons-12938137214845538471'
- runs-on: ubuntu-latest
- timeout-minutes: 30
- permissions:
- contents: write
- steps:
- - name: Harden runner
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
- with:
- egress-policy: audit
-
- - name: Checkout exact feature branch
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- with:
- ref: feat/i18n-workspace-buttons-12938137214845538471
- fetch-depth: 0
- persist-credentials: true
-
- - name: Use repository Node.js contract
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- with:
- node-version: 22.22.3
- cache: npm
-
- - name: Apply reviewed localization cleanup
- run: |
- set -euo pipefail
- python3 -m py_compile scripts/ci/finalize_workspace_i18n.py
- python3 scripts/ci/finalize_workspace_i18n.py
- git show acdbea6344fe1231c39535b575f4de35e4c607c9:package-lock.json > package-lock.json
-
- - name: Verify localization and desktop quality gates
- run: |
- set -euo pipefail
- git diff --check
- python3 -m json.tool apps/desktop/src/locales/en/common.json >/dev/null
- python3 -m json.tool apps/desktop/src/locales/ko/common.json >/dev/null
- grep -F 'text.split(`{${variableName}}`).join(variableValue)' apps/desktop/src/i18n/index.ts >/dev/null
- grep -F 't("thisRole")' apps/desktop/src/features/workspace/Workspace.tsx >/dev/null
- ! grep -F 'Extract
- {t("exportCueSheetCsv" as Extract)}
+ {t("exportCueSheetCsv")}
@@ -348,61 +348,61 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
{activeRole && (
-
{t("stemPlayerLabel" as Extract)}
+
{t("stemPlayerLabel")}
{activeRoleDetails?.name ?? activeRole}
{canTranscribeBass ? (
) : (
)}
diff --git a/apps/desktop/src/i18n/index.test.ts b/apps/desktop/src/i18n/index.test.ts
index dc49a0a25..000c7bea1 100644
--- a/apps/desktop/src/i18n/index.test.ts
+++ b/apps/desktop/src/i18n/index.test.ts
@@ -76,3 +76,21 @@ describe("i18n", () => {
});
});
});
+
+describe("translator placeholder interpolation", () => {
+ it("replaces every occurrence of a named placeholder", () => {
+ 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."
+ );
+ });
+});
diff --git a/apps/desktop/src/i18n/index.ts b/apps/desktop/src/i18n/index.ts
index 4de43a59c..7012a469f 100644
--- a/apps/desktop/src/i18n/index.ts
+++ b/apps/desktop/src/i18n/index.ts
@@ -11,13 +11,16 @@ 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, variables?: Record
): string {
+ return function translate(
+ key: TranslationKey,
+ variables?: Readonly>
+ ): string {
let text = dictionaries[locale][key] ?? dictionaries.en[key];
if (variables) {
- for (const [k, v] of Object.entries(variables)) {
- text = text.replace(new RegExp(`{${k}}`, "g"), v);
+ for (const [variableName, variableValue] of Object.entries(variables)) {
+ text = text.split(`{${variableName}}`).join(variableValue);
}
}
return text;
diff --git a/apps/desktop/src/locales/en/common.json b/apps/desktop/src/locales/en/common.json
index 7a1fdfd34..ee7a37faa 100644
--- a/apps/desktop/src/locales/en/common.json
+++ b/apps/desktop/src/locales/en/common.json
@@ -161,5 +161,6 @@
"soloMuteOthersSoon": "Solo / mute others coming soon",
"transcribeBass": "Transcribe Bass",
"transcribePart": "Transcribe part",
- "transcriptionComingSoon": "{roleName} transcription is coming soon. Bass is ready first."
+ "transcriptionComingSoon": "{roleName} transcription is coming soon. Bass is ready first.",
+ "thisRole": "This role"
}
diff --git a/apps/desktop/src/locales/ko/common.json b/apps/desktop/src/locales/ko/common.json
index edbe2f9cc..a86418056 100644
--- a/apps/desktop/src/locales/ko/common.json
+++ b/apps/desktop/src/locales/ko/common.json
@@ -161,5 +161,6 @@
"soloMuteOthersSoon": "솔로 / 나머지 음소거는 곧 제공됩니다",
"transcribeBass": "베이스 채보",
"transcribePart": "파트 채보",
- "transcriptionComingSoon": "{roleName} 채보는 곧 제공됩니다. 베이스가 먼저 준비되었습니다."
+ "transcriptionComingSoon": "{roleName} 채보는 곧 제공됩니다. 베이스가 먼저 준비되었습니다.",
+ "thisRole": "이 역할"
}
diff --git a/package-lock.json b/package-lock.json
index ff7604e43..cf1c991c1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -955,6 +955,7 @@
"os": [
"aix"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -972,6 +973,7 @@
"os": [
"android"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -989,6 +991,7 @@
"os": [
"android"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1006,6 +1009,7 @@
"os": [
"android"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1023,6 +1027,7 @@
"os": [
"darwin"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1040,6 +1045,7 @@
"os": [
"darwin"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1057,6 +1063,7 @@
"os": [
"freebsd"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1074,6 +1081,7 @@
"os": [
"freebsd"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1091,6 +1099,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1108,6 +1117,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1125,6 +1135,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1142,6 +1153,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1159,6 +1171,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1176,6 +1189,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1193,6 +1207,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1210,6 +1225,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1227,6 +1243,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1244,6 +1261,7 @@
"os": [
"netbsd"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1261,6 +1279,7 @@
"os": [
"netbsd"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1278,6 +1297,7 @@
"os": [
"openbsd"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1295,6 +1315,7 @@
"os": [
"openbsd"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1312,6 +1333,7 @@
"os": [
"openharmony"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1329,6 +1351,7 @@
"os": [
"sunos"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1346,6 +1369,7 @@
"os": [
"win32"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1363,6 +1387,7 @@
"os": [
"win32"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1380,6 +1405,7 @@
"os": [
"win32"
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -7153,9 +7179,9 @@
}
},
"node_modules/undici": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
- "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/scripts/ci/finalize_workspace_i18n.py b/scripts/ci/finalize_workspace_i18n.py
deleted file mode 100644
index b8812a642..000000000
--- a/scripts/ci/finalize_workspace_i18n.py
+++ /dev/null
@@ -1,134 +0,0 @@
-#!/usr/bin/env python3
-"""Finalize Workspace localization safely, add regressions, and self-delete."""
-
-from __future__ import annotations
-
-import json
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-WORKSPACE = ROOT / "apps/desktop/src/features/workspace/Workspace.tsx"
-I18N = ROOT / "apps/desktop/src/i18n/index.ts"
-I18N_TEST = ROOT / "apps/desktop/src/i18n/index.test.ts"
-EN = ROOT / "apps/desktop/src/locales/en/common.json"
-KO = ROOT / "apps/desktop/src/locales/ko/common.json"
-SELF = ROOT / "scripts/ci/finalize_workspace_i18n.py"
-WORKFLOW = ROOT / ".github/workflows/finalize-workspace-i18n.yml"
-
-
-def replace_once(text: str, old: str, new: str, label: str) -> str:
- """Replace exactly one reviewed fragment and fail on branch drift."""
- count = text.count(old)
- if count != 1:
- raise RuntimeError(f"{label}: expected one match, found {count}")
- return text.replace(old, new, 1)
-
-
-def patch_workspace(text: str) -> str:
- """Remove redundant type assertions and localize the fallback role name."""
- assertion = ' as Extract'
- assertion_count = text.count(assertion)
- if assertion_count < 10:
- raise RuntimeError(
- f"workspace translation assertions drifted: expected at least 10, found {assertion_count}"
- )
- text = text.replace(assertion, "")
- return replace_once(
- text,
- 'activeRoleDetails?.name ?? "This role"',
- 'activeRoleDetails?.name ?? t("thisRole")',
- "localized role fallback",
- )
-
-
-def patch_i18n(text: str) -> str:
- """Interpolate literal placeholders without regular-expression semantics."""
- old = '''/** Documented. */
-export function createTranslator(locale: Locale = "en") {
- return function t(key: TranslationKey, variables?: Record): string {
- let text = dictionaries[locale][key] ?? dictionaries.en[key];
- if (variables) {
- for (const [k, v] of Object.entries(variables)) {
- text = text.replace(new RegExp(`{${k}}`, "g"), v);
- }
- }
- return text;
- };
-}
-'''
- new = '''/** Create a locale-bound translator with literal placeholder interpolation. */
-export function createTranslator(locale: Locale = "en") {
- return function translate(
- key: TranslationKey,
- variables?: Readonly>
- ): 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;
- };
-}
-'''
- return replace_once(text, old, new, "translator interpolation")
-
-
-def patch_locale(path: Path, key: str, value: str) -> None:
- """Add one synchronized locale entry without changing existing translations."""
- payload = json.loads(path.read_text(encoding="utf-8"))
- if not isinstance(payload, dict):
- raise RuntimeError(f"locale root must be an object: {path}")
- existing = payload.get(key)
- if existing not in {None, value}:
- raise RuntimeError(f"locale key conflict for {key}: {path}")
- payload[key] = value
- path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
-
-
-def patch_tests(text: str) -> str:
- """Add regression coverage for repeated and literal replacement values."""
- addition = '''
-
-describe("translator placeholder interpolation", () => {
- it("replaces every occurrence of a named placeholder", () => {
- 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."
- );
- });
-});
-'''
- if 'describe("translator placeholder interpolation"' in text:
- raise RuntimeError("translator interpolation regression already exists")
- return text.rstrip() + addition
-
-
-def main() -> int:
- """Apply reviewed localization changes and remove temporary automation files."""
- WORKSPACE.write_text(
- patch_workspace(WORKSPACE.read_text(encoding="utf-8")), encoding="utf-8"
- )
- I18N.write_text(patch_i18n(I18N.read_text(encoding="utf-8")), encoding="utf-8")
- I18N_TEST.write_text(
- patch_tests(I18N_TEST.read_text(encoding="utf-8")), encoding="utf-8"
- )
- patch_locale(EN, "thisRole", "This role")
- patch_locale(KO, "thisRole", "이 역할")
- SELF.unlink()
- WORKFLOW.unlink()
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
From 57ee0c4c5cb9f100951a3183279bd477ae6f0ece Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 4 Aug 2026 12:29:21 +0900
Subject: [PATCH 06/22] fix(deps): prepare bounded undici security update
---
scripts/ci/finalize_undici_security_lock.py | 39 +++++++++++++++++++++
1 file changed, 39 insertions(+)
create mode 100644 scripts/ci/finalize_undici_security_lock.py
diff --git a/scripts/ci/finalize_undici_security_lock.py b/scripts/ci/finalize_undici_security_lock.py
new file mode 100644
index 000000000..075abd5aa
--- /dev/null
+++ b/scripts/ci/finalize_undici_security_lock.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""Apply the minimal Undici security lock update and self-delete."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+LOCK = ROOT / "package-lock.json"
+SELF = ROOT / "scripts/ci/finalize_undici_security_lock.py"
+WORKFLOW = ROOT / ".github/workflows/finalize-undici-security-lock.yml"
+
+OLD = ''' "node_modules/undici": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+'''
+NEW = ''' "node_modules/undici": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+'''
+
+
+def main() -> int:
+ """Replace exactly one reviewed lock entry and remove temporary helpers."""
+ text = LOCK.read_text(encoding="utf-8")
+ if text.count(OLD) != 1:
+ raise RuntimeError(f"expected one Undici 7.28.0 lock entry, found {text.count(OLD)}")
+ if NEW in text:
+ raise RuntimeError("Undici 7.29.0 lock entry already exists")
+ LOCK.write_text(text.replace(OLD, NEW, 1), encoding="utf-8")
+ SELF.unlink()
+ WORKFLOW.unlink()
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
From 3d64e8bb0a35db100b3547c6883cafeee70b53c0 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 4 Aug 2026 12:29:59 +0900
Subject: [PATCH 07/22] ci(deps): verify minimal undici security update
---
.../finalize-undici-security-lock.yml | 83 +++++++++++++++++++
1 file changed, 83 insertions(+)
create mode 100644 .github/workflows/finalize-undici-security-lock.yml
diff --git a/.github/workflows/finalize-undici-security-lock.yml b/.github/workflows/finalize-undici-security-lock.yml
new file mode 100644
index 000000000..28c04a169
--- /dev/null
+++ b/.github/workflows/finalize-undici-security-lock.yml
@@ -0,0 +1,83 @@
+name: Finalize Undici security lock
+
+on:
+ push:
+ branches: [feat/i18n-workspace-buttons-12938137214845538471]
+ paths:
+ - scripts/ci/finalize_undici_security_lock.py
+ - .github/workflows/finalize-undici-security-lock.yml
+ workflow_dispatch:
+
+concurrency:
+ group: finalize-undici-security-lock
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ finalize:
+ if: >-
+ github.repository == 'ContextualWisdomLab/bandscope'
+ && github.actor != 'github-actions[bot]'
+ && github.ref_name == 'feat/i18n-workspace-buttons-12938137214845538471'
+ runs-on: ubuntu-latest
+ timeout-minutes: 25
+ permissions:
+ contents: write
+ steps:
+ - name: Harden runner
+ uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
+ with:
+ egress-policy: audit
+
+ - name: Checkout exact feature branch
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ ref: feat/i18n-workspace-buttons-12938137214845538471
+ fetch-depth: 0
+ persist-credentials: true
+
+ - name: Use repository Node.js contract
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: 22.22.3
+ cache: npm
+
+ - name: Apply reviewed lock update
+ run: |
+ set -euo pipefail
+ python3 -m py_compile scripts/ci/finalize_undici_security_lock.py
+ python3 scripts/ci/finalize_undici_security_lock.py
+
+ - name: Verify bounded dependency change and security gates
+ run: |
+ set -euo pipefail
+ git diff --check
+ python3 -m json.tool package-lock.json >/dev/null
+ git diff --unified=0 acdbea6344fe1231c39535b575f4de35e4c607c9 -- package-lock.json > "$RUNNER_TEMP/lock.diff"
+ grep -F '"version": "7.29.0"' "$RUNNER_TEMP/lock.diff" >/dev/null
+ grep -F 'undici-7.29.0.tgz' "$RUNNER_TEMP/lock.diff" >/dev/null
+ ! grep -F 'apps/desktop/node_modules/' "$RUNNER_TEMP/lock.diff"
+ npm ci --ignore-scripts --no-audit --no-fund
+ npm audit --workspaces --audit-level=high
+ npm run lint --workspace @bandscope/desktop
+ npm run typecheck --workspace @bandscope/desktop
+ npm test --workspace @bandscope/desktop
+ npm run build --workspace @bandscope/desktop
+ test ! -e scripts/ci/finalize_undici_security_lock.py
+ test ! -e .github/workflows/finalize-undici-security-lock.yml
+
+ - name: Commit verified security update
+ run: |
+ set -euo pipefail
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git add -A
+ git diff --cached --check
+ if git diff --cached --quiet; then
+ echo "No dependency changes to commit."
+ exit 0
+ fi
+ git commit -m "fix(deps): update Undici to 7.29.0"
+ git push origin HEAD:refs/heads/feat/i18n-workspace-buttons-12938137214845538471
From 3d041afe6c99dd010c3a87fbd286f194f1b34ba0 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 4 Aug 2026 03:31:03 +0000
Subject: [PATCH 08/22] fix(deps): update Undici to 7.29.0
---
.../finalize-undici-security-lock.yml | 83 -------------------
package-lock.json | 6 +-
scripts/ci/finalize_undici_security_lock.py | 39 ---------
3 files changed, 3 insertions(+), 125 deletions(-)
delete mode 100644 .github/workflows/finalize-undici-security-lock.yml
delete mode 100644 scripts/ci/finalize_undici_security_lock.py
diff --git a/.github/workflows/finalize-undici-security-lock.yml b/.github/workflows/finalize-undici-security-lock.yml
deleted file mode 100644
index 28c04a169..000000000
--- a/.github/workflows/finalize-undici-security-lock.yml
+++ /dev/null
@@ -1,83 +0,0 @@
-name: Finalize Undici security lock
-
-on:
- push:
- branches: [feat/i18n-workspace-buttons-12938137214845538471]
- paths:
- - scripts/ci/finalize_undici_security_lock.py
- - .github/workflows/finalize-undici-security-lock.yml
- workflow_dispatch:
-
-concurrency:
- group: finalize-undici-security-lock
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- finalize:
- if: >-
- github.repository == 'ContextualWisdomLab/bandscope'
- && github.actor != 'github-actions[bot]'
- && github.ref_name == 'feat/i18n-workspace-buttons-12938137214845538471'
- runs-on: ubuntu-latest
- timeout-minutes: 25
- permissions:
- contents: write
- steps:
- - name: Harden runner
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
- with:
- egress-policy: audit
-
- - name: Checkout exact feature branch
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- with:
- ref: feat/i18n-workspace-buttons-12938137214845538471
- fetch-depth: 0
- persist-credentials: true
-
- - name: Use repository Node.js contract
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- with:
- node-version: 22.22.3
- cache: npm
-
- - name: Apply reviewed lock update
- run: |
- set -euo pipefail
- python3 -m py_compile scripts/ci/finalize_undici_security_lock.py
- python3 scripts/ci/finalize_undici_security_lock.py
-
- - name: Verify bounded dependency change and security gates
- run: |
- set -euo pipefail
- git diff --check
- python3 -m json.tool package-lock.json >/dev/null
- git diff --unified=0 acdbea6344fe1231c39535b575f4de35e4c607c9 -- package-lock.json > "$RUNNER_TEMP/lock.diff"
- grep -F '"version": "7.29.0"' "$RUNNER_TEMP/lock.diff" >/dev/null
- grep -F 'undici-7.29.0.tgz' "$RUNNER_TEMP/lock.diff" >/dev/null
- ! grep -F 'apps/desktop/node_modules/' "$RUNNER_TEMP/lock.diff"
- npm ci --ignore-scripts --no-audit --no-fund
- npm audit --workspaces --audit-level=high
- npm run lint --workspace @bandscope/desktop
- npm run typecheck --workspace @bandscope/desktop
- npm test --workspace @bandscope/desktop
- npm run build --workspace @bandscope/desktop
- test ! -e scripts/ci/finalize_undici_security_lock.py
- test ! -e .github/workflows/finalize-undici-security-lock.yml
-
- - name: Commit verified security update
- run: |
- set -euo pipefail
- git config user.name "github-actions[bot]"
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- git add -A
- git diff --cached --check
- if git diff --cached --quiet; then
- echo "No dependency changes to commit."
- exit 0
- fi
- git commit -m "fix(deps): update Undici to 7.29.0"
- git push origin HEAD:refs/heads/feat/i18n-workspace-buttons-12938137214845538471
diff --git a/package-lock.json b/package-lock.json
index cf1c991c1..d18d6deeb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7179,9 +7179,9 @@
}
},
"node_modules/undici": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
- "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/scripts/ci/finalize_undici_security_lock.py b/scripts/ci/finalize_undici_security_lock.py
deleted file mode 100644
index 075abd5aa..000000000
--- a/scripts/ci/finalize_undici_security_lock.py
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env python3
-"""Apply the minimal Undici security lock update and self-delete."""
-
-from __future__ import annotations
-
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-LOCK = ROOT / "package-lock.json"
-SELF = ROOT / "scripts/ci/finalize_undici_security_lock.py"
-WORKFLOW = ROOT / ".github/workflows/finalize-undici-security-lock.yml"
-
-OLD = ''' "node_modules/undici": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
- "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
-'''
-NEW = ''' "node_modules/undici": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
- "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
-'''
-
-
-def main() -> int:
- """Replace exactly one reviewed lock entry and remove temporary helpers."""
- text = LOCK.read_text(encoding="utf-8")
- if text.count(OLD) != 1:
- raise RuntimeError(f"expected one Undici 7.28.0 lock entry, found {text.count(OLD)}")
- if NEW in text:
- raise RuntimeError("Undici 7.29.0 lock entry already exists")
- LOCK.write_text(text.replace(OLD, NEW, 1), encoding="utf-8")
- SELF.unlink()
- WORKFLOW.unlink()
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
From 671d85cdc2ba791502188cba6469170376c1f6b1 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 4 Aug 2026 12:39:27 +0900
Subject: [PATCH 09/22] docs(changelog): record Workspace localization and
Undici fix
---
CHANGELOG.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eea696893..12fb2cf1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
From ffc09729ecc023a913362e106791b843485694fa Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 4 Aug 2026 12:46:16 +0900
Subject: [PATCH 10/22] fix(i18n): prepare reviewed Workspace corrections
---
.../ci/finalize_workspace_review_feedback.py | 154 ++++++++++++++++++
1 file changed, 154 insertions(+)
create mode 100644 scripts/ci/finalize_workspace_review_feedback.py
diff --git a/scripts/ci/finalize_workspace_review_feedback.py b/scripts/ci/finalize_workspace_review_feedback.py
new file mode 100644
index 000000000..217db5810
--- /dev/null
+++ b/scripts/ci/finalize_workspace_review_feedback.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+"""Apply the remaining reviewed Workspace localization fixes and self-delete."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+WORKSPACE = ROOT / "apps/desktop/src/features/workspace/Workspace.tsx"
+WORKSPACE_TEST = ROOT / "apps/desktop/src/features/workspace/Workspace.test.tsx"
+I18N_TEST = ROOT / "apps/desktop/src/i18n/index.test.ts"
+EN = ROOT / "apps/desktop/src/locales/en/common.json"
+SELF = ROOT / "scripts/ci/finalize_workspace_review_feedback.py"
+WORKFLOW = ROOT / ".github/workflows/finalize-workspace-review-feedback.yml"
+
+
+def replace_once(text: str, old: str, new: str, label: str) -> str:
+ """Replace exactly one reviewed fragment and fail closed on branch drift."""
+ count = text.count(old)
+ if count != 1:
+ raise RuntimeError(f"{label}: expected one match, found {count}")
+ return text.replace(old, new, 1)
+
+
+def patch_workspace(text: str) -> str:
+ """Give unavailable transcription controls role-correct visible and accessible copy."""
+ text = replace_once(
+ text,
+ ''' const canTranscribeBass = activeRoleDetails?.name.toLowerCase().includes("bass") ?? false;
+''',
+ ''' const canTranscribeBass = activeRoleDetails?.name.toLowerCase().includes("bass") ?? false;
+ const transcriptionUnavailableLabel = t("transcriptionComingSoon", {
+ roleName: activeRoleDetails?.name ?? t("thisRole")
+ });
+''',
+ "transcription unavailable label",
+ )
+ return replace_once(
+ text,
+ '''
+''',
+ '''
+''',
+ "non-bass transcription button",
+ )
+
+
+def patch_workspace_tests(text: str) -> str:
+ """Pin the English locale and cover role-correct unavailable transcription copy."""
+ text = replace_once(
+ text,
+ ''' it("enables bass transcription from selected role metadata rather than role id text", () => {
+ const song = createDemoRehearsalSong();
+''',
+ ''' it("enables bass transcription from selected role metadata rather than role id text", () => {
+ setNavigatorLanguage("en-US");
+ const song = createDemoRehearsalSong();
+''',
+ "bass transcription locale",
+ )
+ marker = ''' it("renders bass transcription in the dark rehearsal cockpit system", () => {
+'''
+ addition = ''' 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();
+ 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);
+ });
+
+'''
+ if addition in text:
+ raise RuntimeError("non-bass transcription regression already exists")
+ return replace_once(text, marker, addition + marker, "non-bass transcription test")
+
+
+def patch_i18n_test(text: str) -> str:
+ """Name the interpolation regression after the behavior it actually proves."""
+ return replace_once(
+ text,
+ ' it("replaces every occurrence of a named placeholder", () => {',
+ ' it("does not recursively interpolate placeholder text inside replacement values", () => {',
+ "interpolation regression name",
+ )
+
+
+def patch_english_locale(path: Path) -> None:
+ """Replace three incomplete status fragments with complete English sentences."""
+ payload = json.loads(path.read_text(encoding="utf-8"))
+ if not isinstance(payload, dict):
+ raise RuntimeError("English locale root must be an object")
+ replacements = {
+ "playStemSoon": ("Play stem coming soon", "Stem playback is coming soon."),
+ "loopSectionSoon": ("Loop section coming soon", "Section looping is coming soon."),
+ "soloMuteOthersSoon": (
+ "Solo / mute others coming soon",
+ "Solo and mute controls are coming soon.",
+ ),
+ }
+ for key, (expected, replacement) in replacements.items():
+ if payload.get(key) != expected:
+ raise RuntimeError(f"English locale entry drifted: {key}")
+ payload[key] = replacement
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
+
+
+def main() -> int:
+ """Prepare every patch before publishing files and removing temporary helpers."""
+ workspace = patch_workspace(WORKSPACE.read_text(encoding="utf-8"))
+ workspace_test = patch_workspace_tests(WORKSPACE_TEST.read_text(encoding="utf-8"))
+ i18n_test = patch_i18n_test(I18N_TEST.read_text(encoding="utf-8"))
+
+ WORKSPACE.write_text(workspace, encoding="utf-8")
+ WORKSPACE_TEST.write_text(workspace_test, encoding="utf-8")
+ I18N_TEST.write_text(i18n_test, encoding="utf-8")
+ patch_english_locale(EN)
+ SELF.unlink()
+ WORKFLOW.unlink()
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
From 1a7aa004636e48885357dfed4252278d6af64aaa Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 4 Aug 2026 12:46:46 +0900
Subject: [PATCH 11/22] ci(i18n): verify remaining Workspace review fixes
---
.../finalize-workspace-review-feedback.yml | 86 +++++++++++++++++++
1 file changed, 86 insertions(+)
create mode 100644 .github/workflows/finalize-workspace-review-feedback.yml
diff --git a/.github/workflows/finalize-workspace-review-feedback.yml b/.github/workflows/finalize-workspace-review-feedback.yml
new file mode 100644
index 000000000..f3c43d9c6
--- /dev/null
+++ b/.github/workflows/finalize-workspace-review-feedback.yml
@@ -0,0 +1,86 @@
+name: Finalize Workspace review feedback
+
+on:
+ push:
+ branches: [feat/i18n-workspace-buttons-12938137214845538471]
+ paths:
+ - scripts/ci/finalize_workspace_review_feedback.py
+ - .github/workflows/finalize-workspace-review-feedback.yml
+ workflow_dispatch:
+
+concurrency:
+ group: finalize-workspace-review-feedback
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ finalize:
+ if: >-
+ github.repository == 'ContextualWisdomLab/bandscope'
+ && github.actor != 'github-actions[bot]'
+ && github.ref_name == 'feat/i18n-workspace-buttons-12938137214845538471'
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ permissions:
+ contents: write
+ steps:
+ - name: Harden runner
+ uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
+ with:
+ egress-policy: audit
+
+ - name: Checkout exact feature branch
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ ref: feat/i18n-workspace-buttons-12938137214845538471
+ fetch-depth: 0
+ persist-credentials: true
+
+ - name: Use repository Node.js contract
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: 22.22.3
+ cache: npm
+
+ - name: Install lifecycle-disabled dependencies
+ run: npm ci --ignore-scripts --no-audit --no-fund
+
+ - name: Apply reviewed corrections
+ run: |
+ set -euo pipefail
+ python3 -m py_compile scripts/ci/finalize_workspace_review_feedback.py
+ python3 scripts/ci/finalize_workspace_review_feedback.py
+
+ - name: Verify corrected copy, accessibility, and desktop quality
+ run: |
+ set -euo pipefail
+ git diff --check
+ python3 -m json.tool apps/desktop/src/locales/en/common.json >/dev/null
+ python3 -m json.tool apps/desktop/src/locales/ko/common.json >/dev/null
+ grep -F 'aria-label={transcriptionUnavailableLabel}' apps/desktop/src/features/workspace/Workspace.tsx >/dev/null
+ grep -F '{t("transcribePart")}' apps/desktop/src/features/workspace/Workspace.tsx >/dev/null
+ grep -F 'names unavailable transcription for the selected non-bass role' apps/desktop/src/features/workspace/Workspace.test.tsx >/dev/null
+ grep -F 'does not recursively interpolate placeholder text inside replacement values' apps/desktop/src/i18n/index.test.ts >/dev/null
+ grep -F '"playStemSoon": "Stem playback is coming soon."' apps/desktop/src/locales/en/common.json >/dev/null
+ npm run lint --workspace @bandscope/desktop
+ npm run typecheck --workspace @bandscope/desktop
+ npm test --workspace @bandscope/desktop
+ npm run build --workspace @bandscope/desktop
+ test ! -e scripts/ci/finalize_workspace_review_feedback.py
+ test ! -e .github/workflows/finalize-workspace-review-feedback.yml
+
+ - name: Commit verified review fixes
+ run: |
+ set -euo pipefail
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git add -A
+ git diff --cached --check
+ if git diff --cached --quiet; then
+ echo "No review changes to commit."
+ exit 0
+ fi
+ git commit -m "fix(i18n): address Workspace localization review"
+ git push origin HEAD:refs/heads/feat/i18n-workspace-buttons-12938137214845538471
From 87b3455bc84c8aa2a69550991de289e3c4ec92d6 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 4 Aug 2026 03:47:54 +0000
Subject: [PATCH 12/22] fix(i18n): address Workspace localization review
---
.../finalize-workspace-review-feedback.yml | 86 ----------
.../src/features/workspace/Workspace.test.tsx | 20 +++
.../src/features/workspace/Workspace.tsx | 8 +-
apps/desktop/src/i18n/index.test.ts | 2 +-
apps/desktop/src/locales/en/common.json | 6 +-
.../ci/finalize_workspace_review_feedback.py | 154 ------------------
6 files changed, 30 insertions(+), 246 deletions(-)
delete mode 100644 .github/workflows/finalize-workspace-review-feedback.yml
delete mode 100644 scripts/ci/finalize_workspace_review_feedback.py
diff --git a/.github/workflows/finalize-workspace-review-feedback.yml b/.github/workflows/finalize-workspace-review-feedback.yml
deleted file mode 100644
index f3c43d9c6..000000000
--- a/.github/workflows/finalize-workspace-review-feedback.yml
+++ /dev/null
@@ -1,86 +0,0 @@
-name: Finalize Workspace review feedback
-
-on:
- push:
- branches: [feat/i18n-workspace-buttons-12938137214845538471]
- paths:
- - scripts/ci/finalize_workspace_review_feedback.py
- - .github/workflows/finalize-workspace-review-feedback.yml
- workflow_dispatch:
-
-concurrency:
- group: finalize-workspace-review-feedback
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- finalize:
- if: >-
- github.repository == 'ContextualWisdomLab/bandscope'
- && github.actor != 'github-actions[bot]'
- && github.ref_name == 'feat/i18n-workspace-buttons-12938137214845538471'
- runs-on: ubuntu-latest
- timeout-minutes: 30
- permissions:
- contents: write
- steps:
- - name: Harden runner
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
- with:
- egress-policy: audit
-
- - name: Checkout exact feature branch
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- with:
- ref: feat/i18n-workspace-buttons-12938137214845538471
- fetch-depth: 0
- persist-credentials: true
-
- - name: Use repository Node.js contract
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- with:
- node-version: 22.22.3
- cache: npm
-
- - name: Install lifecycle-disabled dependencies
- run: npm ci --ignore-scripts --no-audit --no-fund
-
- - name: Apply reviewed corrections
- run: |
- set -euo pipefail
- python3 -m py_compile scripts/ci/finalize_workspace_review_feedback.py
- python3 scripts/ci/finalize_workspace_review_feedback.py
-
- - name: Verify corrected copy, accessibility, and desktop quality
- run: |
- set -euo pipefail
- git diff --check
- python3 -m json.tool apps/desktop/src/locales/en/common.json >/dev/null
- python3 -m json.tool apps/desktop/src/locales/ko/common.json >/dev/null
- grep -F 'aria-label={transcriptionUnavailableLabel}' apps/desktop/src/features/workspace/Workspace.tsx >/dev/null
- grep -F '{t("transcribePart")}' apps/desktop/src/features/workspace/Workspace.tsx >/dev/null
- grep -F 'names unavailable transcription for the selected non-bass role' apps/desktop/src/features/workspace/Workspace.test.tsx >/dev/null
- grep -F 'does not recursively interpolate placeholder text inside replacement values' apps/desktop/src/i18n/index.test.ts >/dev/null
- grep -F '"playStemSoon": "Stem playback is coming soon."' apps/desktop/src/locales/en/common.json >/dev/null
- npm run lint --workspace @bandscope/desktop
- npm run typecheck --workspace @bandscope/desktop
- npm test --workspace @bandscope/desktop
- npm run build --workspace @bandscope/desktop
- test ! -e scripts/ci/finalize_workspace_review_feedback.py
- test ! -e .github/workflows/finalize-workspace-review-feedback.yml
-
- - name: Commit verified review fixes
- run: |
- set -euo pipefail
- git config user.name "github-actions[bot]"
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- git add -A
- git diff --cached --check
- if git diff --cached --quiet; then
- echo "No review changes to commit."
- exit 0
- fi
- git commit -m "fix(i18n): address Workspace localization review"
- git push origin HEAD:refs/heads/feat/i18n-workspace-buttons-12938137214845538471
diff --git a/apps/desktop/src/features/workspace/Workspace.test.tsx b/apps/desktop/src/features/workspace/Workspace.test.tsx
index aa09f6df0..55cfa6a9b 100644
--- a/apps/desktop/src/features/workspace/Workspace.test.tsx
+++ b/apps/desktop/src/features/workspace/Workspace.test.tsx
@@ -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]!,
@@ -101,6 +102,25 @@ describe("Workspace", () => {
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();
+ 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] = {
diff --git a/apps/desktop/src/features/workspace/Workspace.tsx b/apps/desktop/src/features/workspace/Workspace.tsx
index d5f92ff90..dda6628a4 100644
--- a/apps/desktop/src/features/workspace/Workspace.tsx
+++ b/apps/desktop/src/features/workspace/Workspace.tsx
@@ -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) => {
@@ -397,12 +400,13 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
)}
diff --git a/apps/desktop/src/i18n/index.test.ts b/apps/desktop/src/i18n/index.test.ts
index 000c7bea1..2cac3de86 100644
--- a/apps/desktop/src/i18n/index.test.ts
+++ b/apps/desktop/src/i18n/index.test.ts
@@ -78,7 +78,7 @@ describe("i18n", () => {
});
describe("translator placeholder interpolation", () => {
- it("replaces every occurrence of a named placeholder", () => {
+ it("does not recursively interpolate placeholder text inside replacement values", () => {
const t = createTranslator("en");
expect(
diff --git a/apps/desktop/src/locales/en/common.json b/apps/desktop/src/locales/en/common.json
index ee7a37faa..8ac451add 100644
--- a/apps/desktop/src/locales/en/common.json
+++ b/apps/desktop/src/locales/en/common.json
@@ -154,11 +154,11 @@
"exportHandoffJson": "Export Handoff (JSON)",
"stemPlayerLabel": "Stem Player",
"playStem": "Play stem",
- "playStemSoon": "Play stem coming soon",
+ "playStemSoon": "Stem playback is coming soon.",
"loopSection": "Loop section",
- "loopSectionSoon": "Loop section coming soon",
+ "loopSectionSoon": "Section looping is coming soon.",
"soloMuteOthers": "Solo / mute others",
- "soloMuteOthersSoon": "Solo / mute others coming soon",
+ "soloMuteOthersSoon": "Solo and mute controls are coming soon.",
"transcribeBass": "Transcribe Bass",
"transcribePart": "Transcribe part",
"transcriptionComingSoon": "{roleName} transcription is coming soon. Bass is ready first.",
diff --git a/scripts/ci/finalize_workspace_review_feedback.py b/scripts/ci/finalize_workspace_review_feedback.py
deleted file mode 100644
index 217db5810..000000000
--- a/scripts/ci/finalize_workspace_review_feedback.py
+++ /dev/null
@@ -1,154 +0,0 @@
-#!/usr/bin/env python3
-"""Apply the remaining reviewed Workspace localization fixes and self-delete."""
-
-from __future__ import annotations
-
-import json
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-WORKSPACE = ROOT / "apps/desktop/src/features/workspace/Workspace.tsx"
-WORKSPACE_TEST = ROOT / "apps/desktop/src/features/workspace/Workspace.test.tsx"
-I18N_TEST = ROOT / "apps/desktop/src/i18n/index.test.ts"
-EN = ROOT / "apps/desktop/src/locales/en/common.json"
-SELF = ROOT / "scripts/ci/finalize_workspace_review_feedback.py"
-WORKFLOW = ROOT / ".github/workflows/finalize-workspace-review-feedback.yml"
-
-
-def replace_once(text: str, old: str, new: str, label: str) -> str:
- """Replace exactly one reviewed fragment and fail closed on branch drift."""
- count = text.count(old)
- if count != 1:
- raise RuntimeError(f"{label}: expected one match, found {count}")
- return text.replace(old, new, 1)
-
-
-def patch_workspace(text: str) -> str:
- """Give unavailable transcription controls role-correct visible and accessible copy."""
- text = replace_once(
- text,
- ''' const canTranscribeBass = activeRoleDetails?.name.toLowerCase().includes("bass") ?? false;
-''',
- ''' const canTranscribeBass = activeRoleDetails?.name.toLowerCase().includes("bass") ?? false;
- const transcriptionUnavailableLabel = t("transcriptionComingSoon", {
- roleName: activeRoleDetails?.name ?? t("thisRole")
- });
-''',
- "transcription unavailable label",
- )
- return replace_once(
- text,
- '''