Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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% 커버리지로 추가했습니다.
Comment on lines +10 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

중복된 [Unreleased] heading을 제거하세요.

기존 heading은 line 3에 이미 있습니다. 현재 구조는 markdownlint MD024 경고를 발생시킵니다. 새 i18n 항목을 기존 [Unreleased]Added 목록에 병합하세요.

수정 예시
 ## [Unreleased]

 ### Added
+- i18n 시스템에 변수 보간 기능 구현
+  - `createTranslator`가 두 번째 인자로 변수 객체를 전달받아 `{변수명}`을 치환하도록 개선했습니다.
+  - 관련 테스트 케이스를 100% 커버리지로 추가했습니다.

-## [Unreleased]
-
-### Added
-
-- i18n 시스템에 변수 보간 기능 구현
-  - `createTranslator`가 두 번째 인자로 변수 객체를 전달받아 `{변수명}`을 치환하도록 개선했습니다.
-  - 관련 테스트 케이스를 100% 커버리지로 추가했습니다.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 10-10: Multiple headings with the same content

(MD024, no-duplicate-heading)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 10 - 16, Remove the duplicate [Unreleased] heading
and merge the i18n variable interpolation entry into the existing [Unreleased]
section’s Added list, preserving its details and markdown structure.

Source: Linters/SAST tools



## [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
6 changes: 6 additions & 0 deletions apps/desktop/src/i18n/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
13 changes: 11 additions & 2 deletions apps/desktop/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ 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, string | number>): string {
let result = dictionaries[locale][key] ?? dictionaries.en[key];

if (variables) {
for (const [varName, varValue] of Object.entries(variables)) {
const escapedVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
result = result.replace(new RegExp(`\\{${escapedVarName}\\}`, "g"), String(varValue));
}
}
Comment on lines +19 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
file="apps/desktop/src/i18n/index.ts"
ast-grep outline "$file" --lang typescript
printf '\n--- relevant source ---\n'
sed -n '1,80p' "$file"
printf '\n--- usages and tests ---\n'
rg -n --glob '!node_modules' 'replaceVariables|variables|\\\{[^}]+\\\}' apps/desktop/src/i18n apps/desktop/src | head -200

Repository: ContextualWisdomLab/bandscope

Length of output: 2195


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- tests ---'
sed -n '1,150p' apps/desktop/src/i18n/index.test.ts
printf '%s\n' '--- placeholder inventory ---'
rg -o --no-filename '\{[^{}]+\}' apps/desktop/src/locales | sort | uniq -c | head -200
printf '%s\n' '--- JavaScript behavior probes ---'
node - <<'JS'
const cases = [
  ['dot', 'a.b', 'x {axb} {a.b}', 'VALUE'],
  ['bracket', '[', 'x {[}', 'VALUE'],
  ['replacement-token', 'name', 'x {name}', '$&'],
  ['replacement-dollar', 'name', 'x {name}', '$$'],
  ['replacement-group', 'name', 'x {name}', '$1'],
];
for (const [label, varName, input, value] of cases) {
  try {
    const re = new RegExp(`\\{${varName}\\}`, 'g');
    console.log(label, 'pattern=', re.source, 'output=', input.replace(re, String(value)));
  } catch (error) {
    console.log(label, 'error=', error.name + ': ' + error.message);
  }
}
const input = 'x {a.b} {axb} {name} {[}';
const variables = {'a.b': 'D', name: '$&', '[': 'B'};
console.log('callback=', input.replace(/\{([^{}]+)\}/g, (placeholder, varName) =>
  Object.prototype.hasOwnProperty.call(variables, varName)
    ? String(variables[varName])
    : placeholder
));
JS

Repository: ContextualWisdomLab/bandscope

Length of output: 3548


정규식과 치환 값을 literal-safe하게 처리하세요.

varNameRegExp 소스에 직접 삽입하면 .이 포함된 변수명이 잘못 매칭되고, [ 같은 값은 Invalid regular expression을 발생시킵니다. String(varValue)를 문자열 치환 인자로 전달하면 $&와 같은 replacement token도 해석됩니다.

정적 placeholder 정식 표현식으로 한 번 순회하고, callback에서 값을 반환하세요.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 20-20: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${varName}\\}, "g")
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/i18n/index.ts` around lines 19 - 23, Update the variable
replacement loop in the i18n interpolation logic to use one static
placeholder-matching regular expression rather than constructing a RegExp from
each varName. Replace matches through a callback that looks up the captured
variable name and returns its string value, preserving literal placeholder names
and replacement values including regex metacharacters and tokens such as $&.

Source: Linters/SAST tools


return result;
};
}

Expand Down
46 changes: 10 additions & 36 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading