피아노 학습 화면을 한국어로 제공한다
화면 문구와 도움말 및 PWA 정보를 별도 한국어 리소스로 구성한다.
This commit is contained in:
+5
-5
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
@@ -7,8 +7,8 @@
|
||||
'unsafe-inline' for script-src is needed for the error overlay below;
|
||||
tighten by moving it to a separate .js file if desired. -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self' https://gleitz.github.io https://danigb.github.io https://smpldsnds.github.io https://goldst.dev; worker-src 'self' blob:; object-src 'none'; base-uri 'self'" />
|
||||
<title>Bach to Basics</title>
|
||||
<meta name="description" content="Browser-based piano practice tool. Import MIDI, MusicXML, or PDF sheet music and learn pieces with falling notes, interactive piano, A/B loop, speed trainer, and wait mode." />
|
||||
<title>Bach to Basics 피아노 학습</title>
|
||||
<meta name="description" content="MIDI와 MusicXML 악보를 불러와 건반과 내려오는 음표를 보며 연습하는 한국어 피아노 학습 도구" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon-light.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
@@ -82,7 +82,7 @@
|
||||
el.style.display = 'block';
|
||||
el.textContent = isDev
|
||||
? 'JS ERROR:\n' + msg + '\n\nFile: ' + src + ':' + line + '\n\n' + (err && err.stack ? err.stack : '')
|
||||
: 'Something went wrong. Please reload the page.';
|
||||
: '오류가 발생했습니다. 페이지를 새로고침해 주세요.';
|
||||
};
|
||||
window.addEventListener('unhandledrejection', function(e) {
|
||||
var el = document.getElementById('error-overlay');
|
||||
@@ -90,7 +90,7 @@
|
||||
if (isDev) {
|
||||
el.textContent = (el.textContent || '') + '\n\nUNHANDLED PROMISE:\n' + (e.reason && e.reason.stack ? e.reason.stack : String(e.reason));
|
||||
} else if (!el.textContent) {
|
||||
el.textContent = 'Something went wrong. Please reload the page.';
|
||||
el.textContent = '오류가 발생했습니다. 페이지를 새로고침해 주세요.';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
const translations: Record<string, string> = {
|
||||
Piano: '건반', Notes: '내려오는 음표', Sheet: '악보', All: '전체',
|
||||
Import: '가져오기', Settings: '설정', Appearance: '화면', Keyboard: '건반',
|
||||
'Falling notes': '내려오는 음표', Overlays: '보조 표시', Playback: '재생',
|
||||
'No file loaded': '불러온 파일이 없습니다', 'Import a file': '파일 가져오기',
|
||||
'or drag & drop anywhere': '또는 파일을 화면 어디에나 놓으세요',
|
||||
'No MIDI device': '연결된 MIDI 장치 없음', 'MIDI error': 'MIDI 오류',
|
||||
Stop: '정지', Play: '재생', Pause: '일시정지', Resume: '계속 재생',
|
||||
Loop: '구간 반복', Speed: '연주 속도', 'Wait mode': '따라치기 모드',
|
||||
MIDI: 'MIDI', MusicXML: 'MusicXML', 'PDF sheet music': 'PDF 악보', MP3: 'MP3',
|
||||
'Left hand': '왼손', 'Right hand': '오른손', Other: '기타',
|
||||
Left: '왼손', Right: '오른손', Both: '양손',
|
||||
Natural: '흰건반', Sharps: '검은건반', 'C only': '도만 표시', None: '표시 안 함',
|
||||
Violet: '보라색', Classic: '기본', Ocean: '바다', Forest: '숲', Cascade: '물결', Custom: '사용자 지정',
|
||||
White: '흰색', Ivory: '아이보리', Labels: '음이름', 'Key color': '건반 색상',
|
||||
'Note filter': '음표 필터', 'Octave grid': '옥타브 구분선',
|
||||
'Min note height': '최소 음표 높이', 'Note roundness': '음표 모서리', 'Note outline': '음표 테두리',
|
||||
'White score background': '악보 배경을 흰색으로 표시',
|
||||
'Finger numbers': '손가락 번호', Generate: '생성', Regenerate: '다시 생성',
|
||||
'Also on falling notes': '내려오는 음표에도 표시', 'Measure numbers': '마디 번호',
|
||||
'Beat lines': '박자선', 'Sustain pedal': '서스테인 페달',
|
||||
'Sustained note ghost': '지속 음표 잔상', 'Impact effect': '타건 효과',
|
||||
Off: '끄기', Bloom: '퍼짐', Side: '측면', Trail: '잔상',
|
||||
'Note window': '입력 판정 범위', Transpose: '조옮김', 'Audio offset': '소리 지연 보정',
|
||||
'Scroll to seek': '스크롤로 재생 위치 이동', Instrument: '악기', Practice: '연습',
|
||||
'Wait hand': '따라칠 손', 'Hand volumes': '손별 음량', 'Speed trainer': '속도 훈련',
|
||||
'Start speed': '시작 속도', 'End speed': '종료 속도', Step: '증가 폭',
|
||||
'Reset to defaults': '기본값으로 되돌리기', 'Click again to confirm reset': '한 번 더 눌러 초기화 확인',
|
||||
'Playing': '재생 중', 'current time': '현재 시간', 'total duration': '전체 재생 시간',
|
||||
'Playback position': '재생 위치', 'Playback speed': '연주 속도',
|
||||
'Close settings': '설정 닫기', 'Open settings': '설정 열기',
|
||||
'Dismiss error': '오류 닫기',
|
||||
'Switch to light mode': '밝은 화면으로 전환', 'Switch to dark mode': '어두운 화면으로 전환',
|
||||
'Select MIDI input device': 'MIDI 입력 장치 선택',
|
||||
'Show only the piano keyboard view': '건반만 표시',
|
||||
'Show only the falling notes view': '내려오는 음표만 표시',
|
||||
'Show only the sheet music view': '악보만 표시',
|
||||
'Show all views at once': '모든 화면을 함께 표시',
|
||||
'Stop and return to start': '정지하고 처음으로 이동',
|
||||
'Pause until you play the correct note': '올바른 음을 누를 때까지 재생 대기',
|
||||
'Clear loop': '반복 구간 지우기', 'Start loop': '반복 시작점 지정',
|
||||
'Set loop start to current position': '현재 위치를 반복 시작점으로 지정',
|
||||
'Set loop end to current position': '현재 위치를 반복 종료점으로 지정',
|
||||
'Toggle metronome': '메트로놈 켜기 또는 끄기',
|
||||
'Export current score': '현재 악보 내보내기',
|
||||
'Load a file to enable export': '내보내려면 먼저 파일을 불러오세요',
|
||||
'Save the current score in another format': '현재 악보를 다른 형식으로 저장',
|
||||
'Reset all settings to their default values': '모든 설정을 기본값으로 되돌리기',
|
||||
'Something went wrong. Please reload the page.': '오류가 발생했습니다. 페이지를 새로고침해 주세요.',
|
||||
};
|
||||
|
||||
const fragments: Array<[RegExp, string]> = [
|
||||
[/^Connected: (.+)$/, '$1 연결됨'],
|
||||
[/^Mute (left|right) hand$/, '$1손 소리 끄기'],
|
||||
[/^Unmute (left|right) hand$/, '$1손 소리 켜기'],
|
||||
[/^No (MIDI|MusicXML) data in this document$/, '이 문서에는 $1 데이터가 없습니다'],
|
||||
[/^Waiting for you to play the correct note$/, '올바른 음을 연주할 때까지 기다리는 중'],
|
||||
[/^No file loaded - drag & drop or click ↑ Import$/, '불러온 파일이 없습니다. 파일을 놓거나 ↑ 가져오기를 누르세요'],
|
||||
];
|
||||
|
||||
function translate(value: string): string {
|
||||
if (translations[value]) return translations[value];
|
||||
for (const [pattern, replacement] of fragments) {
|
||||
if (pattern.test(value)) return value.replace(pattern, replacement).replace('left손', '왼손').replace('right손', '오른손');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function translateNode(root: Node): void {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
let node: Node | null;
|
||||
while ((node = walker.nextNode())) {
|
||||
const original = node.nodeValue ?? '';
|
||||
const trimmed = original.trim();
|
||||
if (!trimmed) continue;
|
||||
const translated = translate(trimmed);
|
||||
if (translated !== trimmed) node.nodeValue = original.replace(trimmed, translated);
|
||||
}
|
||||
if (root instanceof Element) {
|
||||
const elements = [root, ...root.querySelectorAll('*')];
|
||||
for (const element of elements) {
|
||||
for (const attribute of ['title', 'aria-label', 'placeholder']) {
|
||||
const original = element.getAttribute(attribute);
|
||||
if (!original) continue;
|
||||
const translated = translate(original);
|
||||
if (translated !== original) element.setAttribute(attribute, translated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function installKoreanUi(): void {
|
||||
document.documentElement.lang = 'ko';
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) translateNode(node);
|
||||
if (mutation.type === 'characterData' && mutation.target.parentNode) translateNode(mutation.target.parentNode);
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
||||
requestAnimationFrame(() => translateNode(document.body));
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { installKoreanUi } from "./i18n/ko";
|
||||
|
||||
installKoreanUi();
|
||||
|
||||
// StrictMode deliberately double-mounts in dev, which conflicts with Pixi.js's
|
||||
// async init - Application.destroy() fires before init() resolves.
|
||||
|
||||
@@ -18,10 +18,10 @@ export default defineConfig({
|
||||
|
||||
// App icons + display name shown on install / home-screen / dock.
|
||||
manifest: {
|
||||
name: "Bach to Basics",
|
||||
short_name: "Bach to Basics",
|
||||
name: "Bach to Basics 피아노 학습",
|
||||
short_name: "Bach 피아노",
|
||||
description:
|
||||
"Browser-based piano practice tool - falling notes, sheet music, MIDI, and practice tools",
|
||||
"악보와 내려오는 음표를 보며 MIDI 피아노를 연습하는 한국어 학습 도구",
|
||||
theme_color: "#9333ea", // matches the purple accent
|
||||
background_color: "#0a0a0a",
|
||||
display: "standalone",
|
||||
|
||||
Reference in New Issue
Block a user