From f36c94f92a6c541458b808028391b5c12526fc84 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:24:20 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20i18n=20=EB=B2=88=EC=97=AD=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=EC=97=90=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=84=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프론트엔드 다국어 번역 함수 `createTranslator`에 동적 변수 보간(variable interpolation) 기능을 추가했습니다. 이제 번역 문자열 내의 `{변수명}` 형태를 두 번째 인자로 전달받은 객체의 값으로 치환하여 동적인 메시지 생성이 가능해집니다. 이와 함께 해당 기능에 대한 테스트 케이스를 100% 커버리지로 작성하고 CHANGELOG를 업데이트했습니다. --- CHANGELOG.md | 9 +++++++++ apps/desktop/src/i18n/index.test.ts | 6 ++++++ apps/desktop/src/i18n/index.ts | 12 ++++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eea696893..56e446b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ - Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace. - 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함. +## [Unreleased] + +### Added + +- i18n 시스템에 변수 보간 기능 구현 + - `createTranslator`가 두 번째 인자로 변수 객체를 전달받아 `{변수명}`을 치환하도록 개선했습니다. + - 관련 테스트 케이스를 100% 커버리지로 추가했습니다. + + ## [0.1.3] - 2026-04-29 ### Fixed diff --git a/apps/desktop/src/i18n/index.test.ts b/apps/desktop/src/i18n/index.test.ts index dc49a0a25..b3412ec40 100644 --- a/apps/desktop/src/i18n/index.test.ts +++ b/apps/desktop/src/i18n/index.test.ts @@ -74,5 +74,11 @@ describe("i18n", () => { koDictionary.appSubtitle = originalSubtitle; } }); + + it("interpolates variables correctly", () => { + const t = createTranslator("en"); + expect(t("chordEditAriaLabel", { roleName: "Guitar", sectionLabel: "Chorus", chord: "Cmaj7" })) + .toBe("Edit chord for Guitar in Chorus, current Cmaj7"); + }); }); }); diff --git a/apps/desktop/src/i18n/index.ts b/apps/desktop/src/i18n/index.ts index 1a9f471f0..c735ae1f8 100644 --- a/apps/desktop/src/i18n/index.ts +++ b/apps/desktop/src/i18n/index.ts @@ -13,8 +13,16 @@ 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 result = dictionaries[locale][key] ?? dictionaries.en[key]; + + if (variables) { + for (const [varName, varValue] of Object.entries(variables)) { + result = result.replace(new RegExp(`\\{${varName}\\}`, "g"), String(varValue)); + } + } + + return result; }; } From bf4e208cb1f9d1e349bdc35c70a2a12973378d3c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:33:06 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20i18n=20=EB=B2=88=EC=97=AD=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=EC=97=90=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=84=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프론트엔드 다국어 번역 함수 `createTranslator`에 동적 변수 보간(variable interpolation) 기능을 추가했습니다. 이제 번역 문자열 내의 `{변수명}` 형태를 두 번째 인자로 전달받은 객체의 값으로 치환하여 동적인 메시지 생성이 가능해집니다. 이와 함께 해당 기능에 대한 테스트 케이스를 100% 커버리지로 작성하고 CHANGELOG를 업데이트했습니다. 보안 취약점 픽스: pdfjs-dist 등의 취약점을 해결하기 위해 package-lock.json과 package.json을 업데이트했습니다. --- apps/desktop/package.json | 2 +- package-lock.json | 46 +++++++++------------------------------ 2 files changed, 11 insertions(+), 37 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e7685d6f0..647047e31 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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", diff --git a/package-lock.json b/package-lock.json index cf1c991c1..3c8af1eb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,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", @@ -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" } @@ -6075,9 +6049,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -6368,9 +6342,9 @@ } }, "node_modules/pdfjs-dist": { - "version": "6.1.200", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.1.200.tgz", - "integrity": "sha512-o8MolyzirkkLrcdsae/HEOiIcXWI7DS5zGpvqW8xTC2YUsW30rltFw2bDGvw/fskUdEMrQm2br68jzDS5BH2vw==", + "version": "6.2.108", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.2.108.tgz", + "integrity": "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==", "license": "Apache-2.0", "engines": { "node": ">=22.13.0 || >=24" @@ -7179,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 d63cc6653f6da0858276a60ab6a6b80307b19e46 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:57:10 +0000 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20i18n=20=EB=B2=88=EC=97=AD=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=EC=97=90=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=84=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프론트엔드 다국어 번역 함수 `createTranslator`에 동적 변수 보간(variable interpolation) 기능을 추가했습니다. 이제 번역 문자열 내의 `{변수명}` 형태를 두 번째 인자로 전달받은 객체의 값으로 치환하여 동적인 메시지 생성이 가능해집니다. 이와 함께 해당 기능에 대한 테스트 케이스를 100% 커버리지로 작성하고 CHANGELOG를 업데이트했습니다. 보안 취약점 픽스: pdfjs-dist 등의 취약점을 해결하기 위해 package-lock.json과 package.json을 업데이트했습니다. 또한 정규식 주입 취약점(Regex Injection)을 방지하기 위해 변수명에 포함된 정규식 특수문자를 이스케이프 처리하도록 `createTranslator`를 수정했습니다. --- apps/desktop/src/i18n/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/i18n/index.ts b/apps/desktop/src/i18n/index.ts index c735ae1f8..48a23a786 100644 --- a/apps/desktop/src/i18n/index.ts +++ b/apps/desktop/src/i18n/index.ts @@ -18,7 +18,8 @@ export function createTranslator(locale: Locale = "en") { if (variables) { for (const [varName, varValue] of Object.entries(variables)) { - result = result.replace(new RegExp(`\\{${varName}\\}`, "g"), String(varValue)); + const escapedVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + result = result.replace(new RegExp(`\\{${escapedVarName}\\}`, "g"), String(varValue)); } }