From 00a99e40879b4e4cca506ed7fe1a5eadf1c70551 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Wed, 22 Jul 2026 02:37:58 +0900 Subject: [PATCH] =?UTF-8?q?refactor(popup):=205=EC=A2=85=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=5Fi18n=20=EC=A3=BC=EC=9E=85=20+=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=ED=86=A0=ED=81=B0=ED=99=94=20(WS-POPUP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 4종 팝업(command/history/result/recording-tip) _i18n 주입: WindowManager getPopupI18nStrings() + 팝업 script applyI18nToHtml() - 한국어/영어 하드코딩 -> popup.* i18n 키 (ko/en 17키) - theme-vars.ts에 --d3-wave-1~9, --d3-status-error/success 토큰 추가 (6 테마) - 팝업 style.css hex -> CSS var 토큰 - packages/i18n getI18n() (main 프로세스용) 추가 SKIP: caption-overlay _i18n(별도 IPC 구조), wave box-shadow 보간(opacity 단순화) 정책: docs/REFACTOR_POLICY.md DP3, 이식 인사이트 P5 --- .../desktop/src/main/windows/WindowManager.ts | 44 ++++++++-- .../renderer/popups/caption-overlay/script.js | 6 +- .../renderer/popups/command-popup/index.html | 10 +-- .../renderer/popups/command-popup/script.js | 12 +++ .../renderer/popups/history-popup/index.html | 6 +- .../renderer/popups/history-popup/script.js | 24 ++++-- .../renderer/popups/recording-tip/script.js | 5 +- .../renderer/popups/recording-tip/style.css | 22 ++--- .../renderer/popups/result-popup/index.html | 2 +- .../renderer/popups/result-popup/script.js | 14 +++ .../renderer/popups/result-popup/style.css | 2 +- packages/i18n/src/index.tsx | 18 ++++ packages/i18n/src/locales/en.json | 20 ++++- packages/i18n/src/locales/ko.json | 20 ++++- packages/ui/src/theme-vars.ts | 86 +++++++++++++++++++ 15 files changed, 256 insertions(+), 35 deletions(-) diff --git a/apps/desktop/src/main/windows/WindowManager.ts b/apps/desktop/src/main/windows/WindowManager.ts index 0c2a7fb..f6d68bd 100644 --- a/apps/desktop/src/main/windows/WindowManager.ts +++ b/apps/desktop/src/main/windows/WindowManager.ts @@ -10,6 +10,7 @@ import { getLogger } from '../services/LoggerService' import { getIsQuitting } from '../lifecycle' import { configGet } from '../services/ConfigService' import { buildPopupThemeCss } from '@d3ro/ui/theme-vars' +import { getI18n } from '@d3ro/i18n' const logger = getLogger('WindowManager') @@ -28,6 +29,39 @@ function injectPopupTheme(win: BrowserWindow): void { }) } +// ── 팝업 i18n 문자열 가져오기 ───────────────────────────── +/** + * 팝업에 주입할 i18n 문자열 맵을 반환. + */ +function getPopupI18nStrings(): Record { + const locale = (configGet('language') as string) || 'ko' + const { t } = getI18n(locale as 'ko' | 'en' | 'ja' | 'zh' | 'zh-TW' | 'es' | 'fr' | 'de' | 'pt' | 'ru' | 'vi' | 'th') + return { + // command-popup + commandSelect: t('popup.commandSelect'), + hintsSelect: t('popup.hints.select'), + hintsApply: t('popup.hints.apply'), + hintsClear: t('popup.hints.clear'), + hintsClose: t('popup.hints.close'), + noCommands: t('popup.noCommands'), + noCommand: t('popup.noCommand'), + // history-popup + hintsSelectEn: t('popup.hints.selectEn'), + hintsPaste: t('popup.hints.paste'), + noHistory: t('popup.noHistory'), + timeJustNow: t('popup.time.justNow'), + timeMinutesAgo: t('popup.time.minutesAgo'), + timeHoursAgo: t('popup.time.hoursAgo'), + timeDaysAgo: t('popup.time.daysAgo'), + // result-popup + copy: t('popup.copy'), + // recording-tip + errorDefault: t('popup.error.default'), + // caption-overlay + captionLoading: t('popup.caption.loading'), + } +} + // ── 윈도우 참조 ─────────────────────────────────────── let mainWindow: BrowserWindow | null = null @@ -181,7 +215,7 @@ export function showRecordingTip( win.setBounds({ x, y, width: TIP_WIDTH, height: TIP_HEIGHT }) // 상태 전송 후 즉시 show (2-phase 제거 — 숨겨진 윈도우의 렌더러 비활성 문제 방지) - win.webContents.send(IPC_CHANNELS.WINDOW.TIP_STATE_CHANGED, { state, ...params }) + win.webContents.send(IPC_CHANNELS.WINDOW.TIP_STATE_CHANGED, { state, _i18n: getPopupI18nStrings(), ...params }) if (!win.isVisible()) { win.showInactive() } @@ -198,7 +232,7 @@ export function updateRecordingTipState( params?: { text?: string; errorMessage?: string } ): void { if (recordingTipWindow && !recordingTipWindow.isDestroyed()) { - recordingTipWindow.webContents.send(IPC_CHANNELS.WINDOW.TIP_STATE_CHANGED, { state, ...params }) + recordingTipWindow.webContents.send(IPC_CHANNELS.WINDOW.TIP_STATE_CHANGED, { state, _i18n: getPopupI18nStrings(), ...params }) } } @@ -268,7 +302,7 @@ export function showResultPopup(text: string, autoHideMs = 5000): void { const win = getResultPopupWindow() // Phase 1: prepare - win.webContents.send(IPC_CHANNELS.POPUP_RESULT.PREPARE, { text }) + win.webContents.send(IPC_CHANNELS.POPUP_RESULT.PREPARE, { text, _i18n: getPopupI18nStrings() }) const handler = (_event: Electron.IpcMainEvent, data: { width: number; height: number }) => { ipcMain.removeListener(IPC_CHANNELS.POPUP_RESULT.MEASURED, handler) @@ -369,7 +403,7 @@ export function showHistoryPopup(entries: Array>): void win.setBounds({ x, y, width: popupWidth, height: popupHeight }) - win.webContents.send(IPC_CHANNELS.POPUP_HISTORY.SHOW_ITEMS, { entries }) + win.webContents.send(IPC_CHANNELS.POPUP_HISTORY.SHOW_ITEMS, { entries, _i18n: getPopupI18nStrings() }) if (!win.isVisible()) { win.showInactive() @@ -456,7 +490,7 @@ export function showCommandPopup(commands: Array>, activ if (y < display.workArea.y) { y = cursorPos.y + 20 } win.setBounds({ x, y, width: popupWidth, height: popupHeight }) - win.webContents.send(IPC_CHANNELS.POPUP_COMMAND.SHOW_ITEMS, { commands, activeId }) + win.webContents.send(IPC_CHANNELS.POPUP_COMMAND.SHOW_ITEMS, { commands, activeId, _i18n: getPopupI18nStrings() }) if (!win.isVisible()) { win.showInactive() } win.webContents.send(IPC_CHANNELS.POPUP_COMMAND.SHOW, {}) diff --git a/apps/desktop/src/renderer/popups/caption-overlay/script.js b/apps/desktop/src/renderer/popups/caption-overlay/script.js index 47edb96..b9a6f92 100644 --- a/apps/desktop/src/renderer/popups/caption-overlay/script.js +++ b/apps/desktop/src/renderer/popups/caption-overlay/script.js @@ -17,6 +17,9 @@ var linesContainer = document.getElementById('lines') var container = document.getElementById('container') + // ── i18n ────────────────────────────────────────────── + var i18nStrings = {} + // ── 상태 ────────────────────────────────────────────── /** @type {Array<{el: HTMLElement, timer: number|null, id: string}>} */ var lines = [] @@ -170,13 +173,14 @@ // 상태 변경 window.popupAPI.on('caption:stateChanged', function (data) { + if (data._i18n) i18nStrings = data._i18n if (data.state === 'starting') { // 로딩 표시 clearAllLines() var loadingEl = document.createElement('div') loadingEl.className = 'caption-line loading' loadingEl.id = 'caption-loading' - loadingEl.textContent = '⏳ Loading STT model...' + loadingEl.textContent = i18nStrings.captionLoading || '⏳ Loading STT model...' loadingEl.style.fontSize = config.fontSize + 'px' linesContainer.appendChild(loadingEl) } else if (data.state === 'active') { diff --git a/apps/desktop/src/renderer/popups/command-popup/index.html b/apps/desktop/src/renderer/popups/command-popup/index.html index ddc49a4..664087f 100644 --- a/apps/desktop/src/renderer/popups/command-popup/index.html +++ b/apps/desktop/src/renderer/popups/command-popup/index.html @@ -9,13 +9,13 @@
-
COMMAND SELECT
+
COMMAND SELECT
- ↑↓ 선택 - ⏎ 적용 - 0 해제 - ESC × + ↑↓ 선택 + ⏎ 적용 + 0 해제 + ESC ×
diff --git a/apps/desktop/src/renderer/popups/command-popup/script.js b/apps/desktop/src/renderer/popups/command-popup/script.js index 95d4a87..f40a72b 100644 --- a/apps/desktop/src/renderer/popups/command-popup/script.js +++ b/apps/desktop/src/renderer/popups/command-popup/script.js @@ -13,6 +13,17 @@ var currentActiveId = null var i18nStrings = {} + // ── i18n 적용 (HTML 텍스트) ─────────────────────────── + function applyI18nToHtml() { + var elements = document.querySelectorAll('[data-i18n]') + elements.forEach(function(el) { + var key = el.getAttribute('data-i18n') + if (i18nStrings[key]) { + el.textContent = i18nStrings[key] + } + }) + } + // ── 아이템 렌더링 ─────────────────────────────────── function renderItems(commands, activeId) { currentCommands = commands @@ -135,6 +146,7 @@ window.popupAPI.on('command:showItems', function (data) { selectedIndex = 0 if (data._i18n) i18nStrings = data._i18n + applyI18nToHtml() renderItems(data.commands || [], data.activeId || null) container.classList.remove('hiding') }) diff --git a/apps/desktop/src/renderer/popups/history-popup/index.html b/apps/desktop/src/renderer/popups/history-popup/index.html index 029efb5..780a1fb 100644 --- a/apps/desktop/src/renderer/popups/history-popup/index.html +++ b/apps/desktop/src/renderer/popups/history-popup/index.html @@ -11,9 +11,9 @@
- ↑↓ Select - ⏎ Paste - ESC × + ↑↓ Select + ⏎ Paste + ESC ×
diff --git a/apps/desktop/src/renderer/popups/history-popup/script.js b/apps/desktop/src/renderer/popups/history-popup/script.js index 68469d9..249d338 100644 --- a/apps/desktop/src/renderer/popups/history-popup/script.js +++ b/apps/desktop/src/renderer/popups/history-popup/script.js @@ -10,18 +10,30 @@ var items = [] var selectedIndex = 0 var currentEntries = [] + var i18nStrings = {} + + // ── i18n 적용 (HTML 텍스트) ─────────────────────────── + function applyI18nToHtml() { + var elements = document.querySelectorAll('[data-i18n]') + elements.forEach(function(el) { + var key = el.getAttribute('data-i18n') + if (i18nStrings[key]) { + el.textContent = i18nStrings[key] + } + }) + } // ── 상대 시간 표시 ─────────────────────────────────── function relativeTime(timestamp) { var diff = Date.now() - timestamp var sec = Math.floor(diff / 1000) - if (sec < 60) return 'just now' + if (sec < 60) return i18nStrings.timeJustNow || 'just now' var min = Math.floor(sec / 60) - if (min < 60) return min + 'm ago' + if (min < 60) return (i18nStrings.timeMinutesAgo || '{{m}}m ago').replace('{{m}}', min) var hr = Math.floor(min / 60) - if (hr < 24) return hr + 'h ago' + if (hr < 24) return (i18nStrings.timeHoursAgo || '{{h}}h ago').replace('{{h}}', hr) var day = Math.floor(hr / 24) - return day + 'd ago' + return (i18nStrings.timeDaysAgo || '{{d}}d ago').replace('{{d}}', day) } // ── 텍스트 truncate ────────────────────────────────── @@ -39,7 +51,7 @@ if (entries.length === 0) { var empty = document.createElement('div') empty.className = 'empty-state' - empty.textContent = 'No history yet' + empty.textContent = i18nStrings.noHistory || 'No history yet' itemsContainer.appendChild(empty) return } @@ -119,6 +131,8 @@ // ── IPC 리스너 ─────────────────────────────────────── window.popupAPI.on('history:showItems', function (data) { selectedIndex = 0 + if (data._i18n) i18nStrings = data._i18n + applyI18nToHtml() renderItems(data.entries || []) container.classList.remove('hiding') measureAndReport() diff --git a/apps/desktop/src/renderer/popups/recording-tip/script.js b/apps/desktop/src/renderer/popups/recording-tip/script.js index 1141f03..9bb5a80 100644 --- a/apps/desktop/src/renderer/popups/recording-tip/script.js +++ b/apps/desktop/src/renderer/popups/recording-tip/script.js @@ -42,6 +42,7 @@ var thinkingStartTime = 0 var thinkingRaf = null var currentState = 'idle' + var i18nStrings = {} // ── 웨이브 바 생성 ─────────────────────────────────── function createWaveBars() { @@ -133,7 +134,7 @@ currentState = 'error' hideAllViews() errorView.classList.remove('hidden') - errorText.textContent = message || '오류가 발생했습니다' + errorText.textContent = message || i18nStrings.errorDefault || '오류가 발생했습니다' } // ── 크기 측정 (2-phase 리사이즈) ──────────────────── @@ -153,6 +154,7 @@ function setupListeners() { // Phase 1: prepare — 숨겨진 상태에서 렌더링 후 크기 측정 window.popupAPI.on('window:tipPrepare', function (data) { + if (data._i18n) i18nStrings = data._i18n var state = data.state if (state === 'recording') showRecording() else if (state === 'thinking') showThinking() @@ -184,6 +186,7 @@ // 상태 변경 (showRecordingTip + updateRecordingTipState 양쪽에서 사용) window.popupAPI.on('window:tipStateChanged', function (data) { + if (data._i18n) i18nStrings = data._i18n var state = data.state if (state === 'recording') showRecording() else if (state === 'thinking') showThinking() diff --git a/apps/desktop/src/renderer/popups/recording-tip/style.css b/apps/desktop/src/renderer/popups/recording-tip/style.css index 5fda14f..d65eaa5 100644 --- a/apps/desktop/src/renderer/popups/recording-tip/style.css +++ b/apps/desktop/src/renderer/popups/recording-tip/style.css @@ -64,16 +64,16 @@ body { min-height: 2px; } -/* 9바 시안→블루→퍼플 스펙트럼 (v2 Midnight — RAW.dark gradient.wave1~3 보간) */ -.wave-bar:nth-child(1) { background: #22d3ee; box-shadow: 0 0 6px rgba(34, 211, 238, 0.35); } -.wave-bar:nth-child(2) { background: #28bef0; box-shadow: 0 0 6px rgba(40, 190, 240, 0.35); } -.wave-bar:nth-child(3) { background: #2fa9f2; box-shadow: 0 0 6px rgba(47, 169, 242, 0.35); } -.wave-bar:nth-child(4) { background: #3596f4; box-shadow: 0 0 6px rgba(53, 150, 244, 0.35); } -.wave-bar:nth-child(5) { background: #3b82f6; box-shadow: 0 0 6px rgba(59, 130, 246, 0.35); } -.wave-bar:nth-child(6) { background: #4f78f6; box-shadow: 0 0 6px rgba(79, 120, 246, 0.35); } -.wave-bar:nth-child(7) { background: #636ff6; box-shadow: 0 0 6px rgba(99, 111, 246, 0.35); } -.wave-bar:nth-child(8) { background: #7765f6; box-shadow: 0 0 6px rgba(119, 101, 246, 0.35); } -.wave-bar:nth-child(9) { background: #8b5cf6; box-shadow: 0 0 6px rgba(139, 92, 246, 0.35); } +/* 9바 시안→블루→퍼플 스펙트럼 (v2 Midnight — 테마 변수 사용) */ +.wave-bar:nth-child(1) { background: var(--d3-wave-1); box-shadow: 0 0 6px var(--d3-wave-1); opacity: 0.9; } +.wave-bar:nth-child(2) { background: var(--d3-wave-2); box-shadow: 0 0 6px var(--d3-wave-2); opacity: 0.92; } +.wave-bar:nth-child(3) { background: var(--d3-wave-3); box-shadow: 0 0 6px var(--d3-wave-3); opacity: 0.94; } +.wave-bar:nth-child(4) { background: var(--d3-wave-4); box-shadow: 0 0 6px var(--d3-wave-4); opacity: 0.96; } +.wave-bar:nth-child(5) { background: var(--d3-wave-5); box-shadow: 0 0 6px var(--d3-wave-5); opacity: 0.98; } +.wave-bar:nth-child(6) { background: var(--d3-wave-6); box-shadow: 0 0 6px var(--d3-wave-6); opacity: 0.98; } +.wave-bar:nth-child(7) { background: var(--d3-wave-7); box-shadow: 0 0 6px var(--d3-wave-7); opacity: 0.96; } +.wave-bar:nth-child(8) { background: var(--d3-wave-8); box-shadow: 0 0 6px var(--d3-wave-8); opacity: 0.94; } +.wave-bar:nth-child(9) { background: var(--d3-wave-9); box-shadow: 0 0 6px var(--d3-wave-9); opacity: 0.92; } .duration { color: var(--d3-text-primary); @@ -111,7 +111,7 @@ body { justify-content: center; width: 18px; height: 18px; - background: #ef4444; + background: var(--d3-status-error); color: white; border-radius: 50%; font-size: 12px; diff --git a/apps/desktop/src/renderer/popups/result-popup/index.html b/apps/desktop/src/renderer/popups/result-popup/index.html index 9055f55..552fe47 100644 --- a/apps/desktop/src/renderer/popups/result-popup/index.html +++ b/apps/desktop/src/renderer/popups/result-popup/index.html @@ -11,7 +11,7 @@
-