diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 89969bd..5b379b7 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -4,10 +4,11 @@ import react from '@vitejs/plugin-react' const sharedAlias = { '@d3ro/core': resolve(__dirname, '../../packages/core/src'), - '@d3ro/ui': resolve(__dirname, '../../packages/ui/src') + '@d3ro/ui': resolve(__dirname, '../../packages/ui/src'), + '@d3ro/i18n': resolve(__dirname, '../../packages/i18n/src/index.tsx') } -const workspaceExclude = ['@d3ro/core', '@d3ro/ui'] +const workspaceExclude = ['@d3ro/core', '@d3ro/ui', '@d3ro/i18n'] export default defineConfig({ main: { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e1113a1..6ac61ac 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -41,6 +41,7 @@ }, "dependencies": { "@d3ro/core": "*", + "@d3ro/i18n": "*", "@d3ro/ui": "*", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", diff --git a/apps/desktop/src/renderer/App.tsx b/apps/desktop/src/renderer/App.tsx index 339dbdf..662cadf 100644 --- a/apps/desktop/src/renderer/App.tsx +++ b/apps/desktop/src/renderer/App.tsx @@ -5,12 +5,23 @@ import { useState, useEffect, useMemo, useRef } from 'react' import { ThemeProvider, CssBaseline, useMediaQuery } from '@mui/material' import { getTheme } from '@d3ro/ui/theme' -import { I18nProvider } from './i18n' +import { I18nProvider, type I18nStorage, type Locale } from '@d3ro/i18n' import { AppLayout } from './components/AppLayout' import { UpgradePromptModal } from './components/UpgradePromptModal' import { startSystemAudioCapture, stopSystemAudioCapture } from './utils/systemAudioCapture' import type { ThemeMode, ConfigChangedEvent } from '@d3ro/core/types' +// Electron ConfigService에 바인딩된 i18n 영속화 어댑터 +const electronI18nStorage: I18nStorage = { + load: async () => { + const result = await window.electronAPI.config.get({ key: 'language' }) + return result.success && result.data ? (result.data as string) : null + }, + save: (locale: Locale) => { + window.electronAPI.config.set({ key: 'language', value: locale }) + } +} + export function App(): React.ReactElement { const [themeMode, setThemeMode] = useState('auto') const prefersDark = useMediaQuery('(prefers-color-scheme: dark)') @@ -60,7 +71,7 @@ export function App(): React.ReactElement { const theme = useMemo(() => getTheme(themeMode, prefersDark), [themeMode, prefersDark]) return ( - + diff --git a/apps/desktop/src/renderer/components/AppLayout.tsx b/apps/desktop/src/renderer/components/AppLayout.tsx index 13ce02f..e91bc34 100644 --- a/apps/desktop/src/renderer/components/AppLayout.tsx +++ b/apps/desktop/src/renderer/components/AppLayout.tsx @@ -24,8 +24,8 @@ import { LicenseModal } from './LicenseModal' import { OnboardingModal } from './OnboardingModal' import { StatusBar } from './StatusBar' import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' -import type { TranslationKey } from '../i18n' +import { useI18n } from '@d3ro/i18n' +import type { TranslationKey } from '@d3ro/i18n' import type { LicenseTier } from '@d3ro/core/types' type Route = 'dashboard' | 'history' | 'dictionary' | 'commands' | 'conversation' | 'knowledge' | 'meeting' diff --git a/apps/desktop/src/renderer/components/FileDropZone.tsx b/apps/desktop/src/renderer/components/FileDropZone.tsx index 2f2ed7c..4a615dc 100644 --- a/apps/desktop/src/renderer/components/FileDropZone.tsx +++ b/apps/desktop/src/renderer/components/FileDropZone.tsx @@ -8,7 +8,7 @@ import CloseIcon from '@mui/icons-material/Close' import UploadFileIcon from '@mui/icons-material/UploadFile' import { MetalCard, PhosphorText, Led } from '@d3ro/ui/components/ds' import { d3roPalette, d3roTypo } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { FileTranscriptionProgress, FileTranscriptionResult, diff --git a/apps/desktop/src/renderer/components/HotkeyRecordModal.tsx b/apps/desktop/src/renderer/components/HotkeyRecordModal.tsx index 690f036..9cd769d 100644 --- a/apps/desktop/src/renderer/components/HotkeyRecordModal.tsx +++ b/apps/desktop/src/renderer/components/HotkeyRecordModal.tsx @@ -16,7 +16,7 @@ import { Typography, } from '@mui/material' import { d3roPalette } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { HotkeyBinding } from '@d3ro/core/types' // ── 키 이름 매핑 (Windows) ────────────────────────────── diff --git a/apps/desktop/src/renderer/components/LicenseModal.tsx b/apps/desktop/src/renderer/components/LicenseModal.tsx index 773596a..5ec0678 100644 --- a/apps/desktop/src/renderer/components/LicenseModal.tsx +++ b/apps/desktop/src/renderer/components/LicenseModal.tsx @@ -23,7 +23,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle' import CancelIcon from '@mui/icons-material/Cancel' import { MetalCard, PhosphorText, Led, ScreenPanel, PhysicalButton } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { LicenseInfo, LicenseTier, TierComparison, UsageQuota } from '@d3ro/core/types' interface LicenseModalProps { diff --git a/apps/desktop/src/renderer/components/LicenseTab.tsx b/apps/desktop/src/renderer/components/LicenseTab.tsx index 8b9d06b..498a2fb 100644 --- a/apps/desktop/src/renderer/components/LicenseTab.tsx +++ b/apps/desktop/src/renderer/components/LicenseTab.tsx @@ -13,7 +13,7 @@ import { import CheckCircleIcon from '@mui/icons-material/CheckCircle' import CancelIcon from '@mui/icons-material/Cancel' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { LicenseInfo, LicenseTier, diff --git a/apps/desktop/src/renderer/components/OllamaGuideModal.tsx b/apps/desktop/src/renderer/components/OllamaGuideModal.tsx index e3b6b74..6abda15 100644 --- a/apps/desktop/src/renderer/components/OllamaGuideModal.tsx +++ b/apps/desktop/src/renderer/components/OllamaGuideModal.tsx @@ -17,7 +17,7 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew' import ContentCopyIcon from '@mui/icons-material/ContentCopy' import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme' import { Led } from '@d3ro/ui/components/ds' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' interface OllamaGuideModalProps { open: boolean diff --git a/apps/desktop/src/renderer/components/OnboardingModal.tsx b/apps/desktop/src/renderer/components/OnboardingModal.tsx index ead7090..4ebc14d 100644 --- a/apps/desktop/src/renderer/components/OnboardingModal.tsx +++ b/apps/desktop/src/renderer/components/OnboardingModal.tsx @@ -18,7 +18,7 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew' import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme' import { Led } from '@d3ro/ui/components/ds' import { HotkeyRecordModal } from './HotkeyRecordModal' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { HotkeyBinding, AudioDevice } from '@d3ro/core/types' interface OnboardingModalProps { diff --git a/apps/desktop/src/renderer/components/ProBadge.tsx b/apps/desktop/src/renderer/components/ProBadge.tsx index 91c4d7a..335f0d6 100644 --- a/apps/desktop/src/renderer/components/ProBadge.tsx +++ b/apps/desktop/src/renderer/components/ProBadge.tsx @@ -6,7 +6,7 @@ import { Box, Typography } from '@mui/material' import LockIcon from '@mui/icons-material/Lock' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme' import { useProFeature } from '../hooks/useProFeature' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { Feature } from '@d3ro/core/types' interface ProBadgeProps { diff --git a/apps/desktop/src/renderer/components/SettingsModal.tsx b/apps/desktop/src/renderer/components/SettingsModal.tsx index 75bb321..31336fc 100644 --- a/apps/desktop/src/renderer/components/SettingsModal.tsx +++ b/apps/desktop/src/renderer/components/SettingsModal.tsx @@ -34,8 +34,8 @@ import CancelIcon from '@mui/icons-material/Cancel' import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme' import { HotkeyRecordModal } from './HotkeyRecordModal' import { LicenseTab } from './LicenseTab' -import { useI18n, LOCALE_META } from '../i18n' -import type { Locale } from '../i18n' +import { useI18n, LOCALE_META } from '@d3ro/i18n' +import type { Locale } from '@d3ro/i18n' import type { ThemeMode, AppConfig, HotkeyBinding, AudioDevice } from '@d3ro/core/types' import { Feature } from '@d3ro/core/types' diff --git a/apps/desktop/src/renderer/components/StatusBar.tsx b/apps/desktop/src/renderer/components/StatusBar.tsx index 8b6e99b..1d33995 100644 --- a/apps/desktop/src/renderer/components/StatusBar.tsx +++ b/apps/desktop/src/renderer/components/StatusBar.tsx @@ -8,7 +8,7 @@ import CloseIcon from '@mui/icons-material/Close' import { Led } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '@d3ro/ui/theme' import { OllamaGuideModal } from './OllamaGuideModal' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { LLMStatus } from '@d3ro/core/types' export function StatusBar(): React.ReactElement { diff --git a/apps/desktop/src/renderer/components/TemplateSection.tsx b/apps/desktop/src/renderer/components/TemplateSection.tsx index bf07789..bfefd01 100644 --- a/apps/desktop/src/renderer/components/TemplateSection.tsx +++ b/apps/desktop/src/renderer/components/TemplateSection.tsx @@ -10,7 +10,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow' import { MetalCard, PhosphorText, Led, PhysicalButton } from '@d3ro/ui/components/ds' import { PageHeader, EmptyStateCard } from './shared' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { DictationTemplate, TemplateField, TemplateSessionInfo } from '@d3ro/core/types' export function TemplateSection(): React.ReactElement { diff --git a/apps/desktop/src/renderer/components/UpgradePromptModal.tsx b/apps/desktop/src/renderer/components/UpgradePromptModal.tsx index b53975b..16e5c2d 100644 --- a/apps/desktop/src/renderer/components/UpgradePromptModal.tsx +++ b/apps/desktop/src/renderer/components/UpgradePromptModal.tsx @@ -15,7 +15,7 @@ import { import LockIcon from '@mui/icons-material/Lock' import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { UpgradePromptEvent, UsageQuota } from '@d3ro/core/types' export function UpgradePromptModal(): React.ReactElement { diff --git a/apps/desktop/src/renderer/components/meeting/AddDocumentDialog.tsx b/apps/desktop/src/renderer/components/meeting/AddDocumentDialog.tsx index 80b0824..410705f 100644 --- a/apps/desktop/src/renderer/components/meeting/AddDocumentDialog.tsx +++ b/apps/desktop/src/renderer/components/meeting/AddDocumentDialog.tsx @@ -15,7 +15,7 @@ import { MetalCard } from '@d3ro/ui/components/ds' import { PhosphorText } from '@d3ro/ui/components/ds' import { PhysicalButton } from '@d3ro/ui/components/ds' import { d3roPalette, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme' -import { useI18n } from '../../i18n' +import { useI18n } from '@d3ro/i18n' interface TemplateItem { id: string diff --git a/apps/desktop/src/renderer/components/meeting/DocumentTab.tsx b/apps/desktop/src/renderer/components/meeting/DocumentTab.tsx index 51e1732..3336e6c 100644 --- a/apps/desktop/src/renderer/components/meeting/DocumentTab.tsx +++ b/apps/desktop/src/renderer/components/meeting/DocumentTab.tsx @@ -8,7 +8,7 @@ import { MarkdownEditor } from './MarkdownEditor' import { ExportMenu } from './ExportMenu' import { PhysicalButton } from '@d3ro/ui/components/ds' import { d3roPalette, d3roTypo } from '@d3ro/ui/theme' -import { useI18n } from '../../i18n' +import { useI18n } from '@d3ro/i18n' import type { MeetingExportFormat } from '@d3ro/core/types' interface DocumentTabDoc { diff --git a/apps/desktop/src/renderer/components/meeting/EditableSegment.tsx b/apps/desktop/src/renderer/components/meeting/EditableSegment.tsx index 7a92483..813df38 100644 --- a/apps/desktop/src/renderer/components/meeting/EditableSegment.tsx +++ b/apps/desktop/src/renderer/components/meeting/EditableSegment.tsx @@ -4,7 +4,7 @@ import { useState, useRef, useCallback } from 'react' import { Box, TextField, Tooltip, Chip } from '@mui/material' import { d3roPalette, d3roFontMono } from '@d3ro/ui/theme' -import { useI18n } from '../../i18n' +import { useI18n } from '@d3ro/i18n' // Phase 15.5: 화자별 색상 매핑 (d3roPalette SSOT) const SPEAKER_COLORS = [ diff --git a/apps/desktop/src/renderer/components/meeting/ExportMenu.tsx b/apps/desktop/src/renderer/components/meeting/ExportMenu.tsx index db5a243..ca8836d 100644 --- a/apps/desktop/src/renderer/components/meeting/ExportMenu.tsx +++ b/apps/desktop/src/renderer/components/meeting/ExportMenu.tsx @@ -6,7 +6,7 @@ import { Menu, MenuItem } from '@mui/material' import FileDownloadIcon from '@mui/icons-material/FileDownload' import { PhysicalButton } from '@d3ro/ui/components/ds' import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius, d3roShadow } from '@d3ro/ui/theme' -import { useI18n } from '../../i18n' +import { useI18n } from '@d3ro/i18n' import type { MeetingExportFormat } from '@d3ro/core/types' interface ExportMenuProps { diff --git a/apps/desktop/src/renderer/components/meeting/MarkdownEditor.tsx b/apps/desktop/src/renderer/components/meeting/MarkdownEditor.tsx index bcdce8b..628a626 100644 --- a/apps/desktop/src/renderer/components/meeting/MarkdownEditor.tsx +++ b/apps/desktop/src/renderer/components/meeting/MarkdownEditor.tsx @@ -6,7 +6,7 @@ import { Box } from '@mui/material' import { MarkdownRenderer } from './MarkdownRenderer' import { PhysicalButton } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '@d3ro/ui/theme' -import { useI18n } from '../../i18n' +import { useI18n } from '@d3ro/i18n' interface MarkdownEditorProps { content: string diff --git a/apps/desktop/src/renderer/components/meeting/MeetingChatPanel.tsx b/apps/desktop/src/renderer/components/meeting/MeetingChatPanel.tsx index 0df913c..b246e7e 100644 --- a/apps/desktop/src/renderer/components/meeting/MeetingChatPanel.tsx +++ b/apps/desktop/src/renderer/components/meeting/MeetingChatPanel.tsx @@ -10,7 +10,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { PhosphorText } from '@d3ro/ui/components/ds' import { PhysicalButton } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow } from '@d3ro/ui/theme' -import { useI18n } from '../../i18n' +import { useI18n } from '@d3ro/i18n' import type { MeetingChatMessage } from '@d3ro/core/types' interface MeetingChatPanelProps { diff --git a/apps/desktop/src/renderer/components/meeting/MeetingDetailTabs.tsx b/apps/desktop/src/renderer/components/meeting/MeetingDetailTabs.tsx index 2a99bc0..5bf20b0 100644 --- a/apps/desktop/src/renderer/components/meeting/MeetingDetailTabs.tsx +++ b/apps/desktop/src/renderer/components/meeting/MeetingDetailTabs.tsx @@ -20,7 +20,7 @@ import { AddDocumentDialog } from './AddDocumentDialog' import { MeetingChatPanel } from './MeetingChatPanel' import { PhosphorText } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme' -import { useI18n } from '../../i18n' +import { useI18n } from '@d3ro/i18n' import type { MeetingSessionDetail, MeetingDocument, diff --git a/apps/desktop/src/renderer/components/meeting/TranscriptTab.tsx b/apps/desktop/src/renderer/components/meeting/TranscriptTab.tsx index 5a8798e..591b27e 100644 --- a/apps/desktop/src/renderer/components/meeting/TranscriptTab.tsx +++ b/apps/desktop/src/renderer/components/meeting/TranscriptTab.tsx @@ -7,7 +7,7 @@ import { EditableSegment } from './EditableSegment' import { PhysicalButton } from '@d3ro/ui/components/ds' import { PhosphorText } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme' -import { useI18n } from '../../i18n' +import { useI18n } from '@d3ro/i18n' import FileDownloadIcon from '@mui/icons-material/FileDownload' import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh' import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver' diff --git a/apps/desktop/src/renderer/components/shared/HistoryEntryCard.tsx b/apps/desktop/src/renderer/components/shared/HistoryEntryCard.tsx index 1991823..af32033 100644 --- a/apps/desktop/src/renderer/components/shared/HistoryEntryCard.tsx +++ b/apps/desktop/src/renderer/components/shared/HistoryEntryCard.tsx @@ -12,7 +12,7 @@ import SummarizeIcon from '@mui/icons-material/Summarize' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { MetalCard, Led } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme' -import { useI18n } from '../../i18n' +import { useI18n } from '@d3ro/i18n' import { formatDuration } from '../../utils/formatters' import type { HistoryEntry, MemoTag, MeetingSummaryResult } from '@d3ro/core/types' diff --git a/apps/desktop/src/renderer/pages/CommandsPage.tsx b/apps/desktop/src/renderer/pages/CommandsPage.tsx index f68ebfb..615e997 100644 --- a/apps/desktop/src/renderer/pages/CommandsPage.tsx +++ b/apps/desktop/src/renderer/pages/CommandsPage.tsx @@ -13,7 +13,7 @@ import LinkIcon from '@mui/icons-material/Link' import { MetalCard, PhosphorText, Led, ScreenPanel } from '@d3ro/ui/components/ds' import { PageHeader, EmptyStateCard } from '../components/shared' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import { TemplateSection } from '../components/TemplateSection' import type { VoiceCommandRule, VoiceCommandKeyword, KeywordMatchMode, LLMChain, ChainStep } from '@d3ro/core/types' diff --git a/apps/desktop/src/renderer/pages/DashboardPage.tsx b/apps/desktop/src/renderer/pages/DashboardPage.tsx index 8400db1..1710305 100644 --- a/apps/desktop/src/renderer/pages/DashboardPage.tsx +++ b/apps/desktop/src/renderer/pages/DashboardPage.tsx @@ -7,7 +7,7 @@ import { Box } from '@mui/material' import { CrtDisplay, InstrumentPanel, Led, MetalCard, PhosphorText, ScreenPanel, ButtonGroup, PhysicalButton } from '@d3ro/ui/components/ds' import { EmptyStateCard, HistoryEntryCard } from '../components/shared' import { d3roPalette, d3roTypo } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import { formatRecordingTime, formatRecordingTimeUnit, formatNumber, getDateKey } from '../utils/formatters' import { FileDropZone } from '../components/FileDropZone' import type { StatsSummary, HistoryEntry, HotkeyBinding, CaptionState, LicenseTier, UsageQuota } from '@d3ro/core/types' diff --git a/apps/desktop/src/renderer/pages/DictionaryPage.tsx b/apps/desktop/src/renderer/pages/DictionaryPage.tsx index c8a1847..d0b8b4c 100644 --- a/apps/desktop/src/renderer/pages/DictionaryPage.tsx +++ b/apps/desktop/src/renderer/pages/DictionaryPage.tsx @@ -9,7 +9,7 @@ import EditIcon from '@mui/icons-material/Edit' import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds' import { PageHeader, SearchInput, EmptyStateCard } from '../components/shared' import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { DictionaryEntry, DictionaryPage as DictPageData } from '@d3ro/core/types' const PAGE_SIZE = 50 diff --git a/apps/desktop/src/renderer/pages/HistoryPage.tsx b/apps/desktop/src/renderer/pages/HistoryPage.tsx index c11dd3b..9bb2563 100644 --- a/apps/desktop/src/renderer/pages/HistoryPage.tsx +++ b/apps/desktop/src/renderer/pages/HistoryPage.tsx @@ -7,7 +7,7 @@ import { Box, Chip, IconButton, Tooltip } from '@mui/material' import FileDownloadIcon from '@mui/icons-material/FileDownload' import { PhosphorText } from '@d3ro/ui/components/ds' import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import { getDateKey } from '../utils/formatters' import { EmptyStateCard, SearchInput, PageHeader, HistoryEntryCard } from '../components/shared' import type { HistoryEntry, HistoryPage as HistoryPageData, TagCount } from '@d3ro/core/types' diff --git a/apps/desktop/src/renderer/pages/KnowledgeBasePage.tsx b/apps/desktop/src/renderer/pages/KnowledgeBasePage.tsx index f125b85..5a20efd 100644 --- a/apps/desktop/src/renderer/pages/KnowledgeBasePage.tsx +++ b/apps/desktop/src/renderer/pages/KnowledgeBasePage.tsx @@ -13,7 +13,7 @@ import DescriptionIcon from '@mui/icons-material/Description' import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '@d3ro/ui/components/ds' import { PageHeader, EmptyStateCard } from '../components/shared' import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { RAGDocument, RAGQueryResult, RAGIndexProgress } from '@d3ro/core/types' export function KnowledgeBasePage(): React.ReactElement { diff --git a/apps/desktop/src/renderer/pages/MeetingModePage.tsx b/apps/desktop/src/renderer/pages/MeetingModePage.tsx index 672cbaa..d223bbb 100644 --- a/apps/desktop/src/renderer/pages/MeetingModePage.tsx +++ b/apps/desktop/src/renderer/pages/MeetingModePage.tsx @@ -17,7 +17,7 @@ import { MeetingDetailTabs } from '../components/meeting/MeetingDetailTabs' import { EditableSegment } from '../components/meeting/EditableSegment' import { PageHeader, EmptyStateCard } from '../components/shared' import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { MeetingSessionSummary, MeetingSessionDetail, diff --git a/apps/desktop/src/renderer/pages/VoiceConversationPage.tsx b/apps/desktop/src/renderer/pages/VoiceConversationPage.tsx index 483787b..ddb41f0 100644 --- a/apps/desktop/src/renderer/pages/VoiceConversationPage.tsx +++ b/apps/desktop/src/renderer/pages/VoiceConversationPage.tsx @@ -12,7 +12,7 @@ import CancelIcon from '@mui/icons-material/Cancel' import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel, InstrumentPanel } from '@d3ro/ui/components/ds' import { PageHeader } from '../components/shared' import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme' -import { useI18n } from '../i18n' +import { useI18n } from '@d3ro/i18n' import type { ConversationState, ConversationMessage, diff --git a/apps/desktop/tsconfig.node.json b/apps/desktop/tsconfig.node.json index 649f4f7..592d492 100644 --- a/apps/desktop/tsconfig.node.json +++ b/apps/desktop/tsconfig.node.json @@ -8,7 +8,8 @@ "@d3ro/core": ["../../packages/core/src/index.ts"], "@d3ro/core/*": ["../../packages/core/src/*"], "@d3ro/ui": ["../../packages/ui/src/index.ts"], - "@d3ro/ui/*": ["../../packages/ui/src/*"] + "@d3ro/ui/*": ["../../packages/ui/src/*"], + "@d3ro/i18n": ["../../packages/i18n/src/index.tsx"] }, "strict": true, "noImplicitAny": true, diff --git a/apps/desktop/tsconfig.web.json b/apps/desktop/tsconfig.web.json index e6f5ac8..695b721 100644 --- a/apps/desktop/tsconfig.web.json +++ b/apps/desktop/tsconfig.web.json @@ -8,7 +8,8 @@ "@d3ro/core": ["../../packages/core/src/index.ts"], "@d3ro/core/*": ["../../packages/core/src/*"], "@d3ro/ui": ["../../packages/ui/src/index.ts"], - "@d3ro/ui/*": ["../../packages/ui/src/*"] + "@d3ro/ui/*": ["../../packages/ui/src/*"], + "@d3ro/i18n": ["../../packages/i18n/src/index.tsx"] }, "jsx": "react-jsx", "strict": true, diff --git a/apps/desktop/vitest.config.ts b/apps/desktop/vitest.config.ts index 685360b..3618c42 100644 --- a/apps/desktop/vitest.config.ts +++ b/apps/desktop/vitest.config.ts @@ -18,7 +18,8 @@ export default defineConfig({ resolve: { alias: { '@d3ro/core': resolve(__dirname, '../../packages/core/src'), - '@d3ro/ui': resolve(__dirname, '../../packages/ui/src') + '@d3ro/ui': resolve(__dirname, '../../packages/ui/src'), + '@d3ro/i18n': resolve(__dirname, '../../packages/i18n/src/index.tsx') } } }) diff --git a/memory/project_status.md b/memory/project_status.md index df17256..6eb478f 100644 --- a/memory/project_status.md +++ b/memory/project_status.md @@ -6,10 +6,12 @@ ## 현재 단계 **V1 (Electron) 완료** → **V2 (Monorepo) 진행 중** -- Phase V2-1a ✅ 완료 (Monorepo 구조 이동) -- Phase V2-1b ✅ 완료 (packages/core 추출) -- Phase V2-1c ✅ 완료 (packages/ui 추출) -- 다음: Phase V2-1d (packages/i18n 추출) +- Phase V2-1 (전체) ✅ 완료 + - V2-1a: Monorepo 구조 이동 + - V2-1b: packages/core 추출 + - V2-1c: packages/ui 추출 + - V2-1d: packages/i18n 추출 +- 다음: Phase V2-2 (Supabase 인프라) ## V1 완료 페이즈 @@ -40,7 +42,28 @@ | V2-1a | npm workspaces + apps/desktop으로 V1 이동 | ✅ 완료 | | V2-1b | packages/core 추출 (types, errors, ipc-channels, constants, utils) | ✅ 완료 | | V2-1c | packages/ui 추출 (DS 컴포넌트 + theme + theme-vars) | ✅ 완료 | -| V2-1d | packages/i18n 추출 (locale JSON + 훅) | ⏸️ 대기 | +| V2-1d | packages/i18n 추출 (locale JSON + 훅) | ✅ 완료 | + +**V2-1d 완료 내역** +- `packages/i18n/` (@d3ro/i18n) 신규 생성: + - `src/locales/` 12개 JSON (ko/en/ja/zh/zh-TW/es/fr/de/pt/ru/vi/th) + - `src/index.tsx`: TFunction, Locale, LOCALE_META, resolveTranslation, createFormatDate/Number/RelativeDate/Time, I18nContext, useI18n, I18nProvider +- **Electron 독립성 확보**: `I18nStorage` interface 추가 (load/save 어댑터) + - 기존 `window.electronAPI.config` 직접 호출 → `storage` prop 주입 패턴 + - 기본값은 noopStorage (메모리 한정) +- `apps/desktop/src/renderer/App.tsx`에 `electronI18nStorage` 어댑터 구현 후 ``로 주입 +- apps/desktop 설정: + - `@d3ro/i18n: "*"` dep 추가 + - tsconfig paths, vite alias, externalize exclude에 `@d3ro/i18n` 추가 +- **일괄 치환**: `./i18n`, `../i18n`, `../../i18n` → `@d3ro/i18n` (29 파일) +- `apps/desktop/src/renderer/i18n/` 디렉토리 완전 제거 + +**V2-1d 검증** +- typecheck ✅ +- build ✅ +- dev 런타임 ✅ (DB/핫키/Ollama/Main window 모두 정상) +- Electron 의존이 완전히 apps/desktop에만 남아 monorepo 원칙 준수 + (packages/i18n은 React에만 의존, 웹/모바일에서 그대로 재사용 가능) **V2-1c 완료 내역** - `packages/ui/` (@d3ro/ui) 신규 생성: diff --git a/package-lock.json b/package-lock.json index 9b611e0..9b5b839 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "license": "MIT", "dependencies": { "@d3ro/core": "*", + "@d3ro/i18n": "*", "@d3ro/ui": "*", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", @@ -400,6 +401,10 @@ "resolved": "apps/desktop", "link": true }, + "node_modules/@d3ro/i18n": { + "resolved": "packages/i18n", + "link": true + }, "node_modules/@d3ro/ui": { "resolved": "packages/ui", "link": true @@ -12820,6 +12825,17 @@ "@types/node": "^22.13.0" } }, + "packages/i18n": { + "name": "@d3ro/i18n", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@types/react": "^19.0.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, "packages/ui": { "name": "@d3ro/ui", "version": "1.0.0", diff --git a/packages/i18n/package.json b/packages/i18n/package.json new file mode 100644 index 0000000..1da99ea --- /dev/null +++ b/packages/i18n/package.json @@ -0,0 +1,21 @@ +{ + "name": "@d3ro/i18n", + "version": "1.0.0", + "private": true, + "description": "D3RO Voice 공유 i18n — 12개 locale, 타입 안전 키, Context, 포맷 유틸", + "license": "MIT", + "main": "./src/index.tsx", + "types": "./src/index.tsx", + "exports": { + ".": { + "types": "./src/index.tsx", + "default": "./src/index.tsx" + } + }, + "peerDependencies": { + "react": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0" + } +} diff --git a/apps/desktop/src/renderer/i18n/index.tsx b/packages/i18n/src/index.tsx similarity index 80% rename from apps/desktop/src/renderer/i18n/index.tsx rename to packages/i18n/src/index.tsx index 1bd6413..f0ddaf8 100644 --- a/apps/desktop/src/renderer/i18n/index.tsx +++ b/packages/i18n/src/index.tsx @@ -1,21 +1,22 @@ -// src/renderer/i18n/index.ts +// packages/i18n/src/index.tsx // SSOT i18n 엔진: 타입 안전 키, React Context, fallback 체인, Intl 포맷팅 // 마스터: ko.json — 모든 키의 단일 소스 +// Electron 독립: 로케일 영속화는 I18nStorage prop으로 주입받음. import { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react' import type { ReactNode } from 'react' -import ko from './ko.json' -import en from './en.json' -import ja from './ja.json' -import zh from './zh.json' -import zhTW from './zh-TW.json' -import es from './es.json' -import fr from './fr.json' -import de from './de.json' -import pt from './pt.json' -import ru from './ru.json' -import vi from './vi.json' -import th from './th.json' +import ko from './locales/ko.json' +import en from './locales/en.json' +import ja from './locales/ja.json' +import zh from './locales/zh.json' +import zhTW from './locales/zh-TW.json' +import es from './locales/es.json' +import fr from './locales/fr.json' +import de from './locales/de.json' +import pt from './locales/pt.json' +import ru from './locales/ru.json' +import vi from './locales/vi.json' +import th from './locales/th.json' // ── 타입 ──────────────────────────────────────────────── /** 마스터 키에서 자동 추출된 번역 키 유니온 */ @@ -147,29 +148,52 @@ export interface I18nContextValue { const I18nContext = createContext(null) +// ── Storage 추상화 ──────────────────────────────────── +/** + * 로케일 영속화 어댑터. 앱별로 주입 (Electron config, localStorage, etc). + * 기본 no-op: 메모리 기반으로만 동작. + */ +export interface I18nStorage { + load: () => Promise | string | null + save: (locale: Locale) => void | Promise +} + +const noopStorage: I18nStorage = { + load: () => null, + save: () => {} +} + // ── Provider ─────────────────────────────────────────── export interface I18nProviderProps { initialLocale?: Locale + /** 로케일 영속화 어댑터. 없으면 메모리 기반(세션 한정). */ + storage?: I18nStorage children: ReactNode } -export function I18nProvider({ initialLocale = 'ko', children }: I18nProviderProps): React.ReactElement { +export function I18nProvider({ + initialLocale = 'ko', + storage = noopStorage, + children +}: I18nProviderProps): React.ReactElement { const [locale, setLocaleState] = useState(initialLocale) - // ConfigService에서 저장된 언어 로드 + // storage에서 저장된 언어 로드 useEffect(() => { - window.electronAPI.config.get({ key: 'language' }).then((r) => { - if (r.success && r.data && isValidLocale(r.data as string)) { - setLocaleState(r.data as Locale) + Promise.resolve(storage.load()).then((loaded) => { + if (loaded && isValidLocale(loaded)) { + setLocaleState(loaded) } }) - }, []) + }, [storage]) - const setLocale = useCallback((newLocale: Locale) => { - setLocaleState(newLocale) - // 설정에 저장 - window.electronAPI.config.set({ key: 'language', value: newLocale }) - }, []) + const setLocale = useCallback( + (newLocale: Locale) => { + setLocaleState(newLocale) + void storage.save(newLocale) + }, + [storage] + ) const value = useMemo((): I18nContextValue => { const translations = LOCALE_MAP[locale] ?? ko diff --git a/apps/desktop/src/renderer/i18n/de.json b/packages/i18n/src/locales/de.json similarity index 100% rename from apps/desktop/src/renderer/i18n/de.json rename to packages/i18n/src/locales/de.json diff --git a/apps/desktop/src/renderer/i18n/en.json b/packages/i18n/src/locales/en.json similarity index 100% rename from apps/desktop/src/renderer/i18n/en.json rename to packages/i18n/src/locales/en.json diff --git a/apps/desktop/src/renderer/i18n/es.json b/packages/i18n/src/locales/es.json similarity index 100% rename from apps/desktop/src/renderer/i18n/es.json rename to packages/i18n/src/locales/es.json diff --git a/apps/desktop/src/renderer/i18n/fr.json b/packages/i18n/src/locales/fr.json similarity index 100% rename from apps/desktop/src/renderer/i18n/fr.json rename to packages/i18n/src/locales/fr.json diff --git a/apps/desktop/src/renderer/i18n/ja.json b/packages/i18n/src/locales/ja.json similarity index 100% rename from apps/desktop/src/renderer/i18n/ja.json rename to packages/i18n/src/locales/ja.json diff --git a/apps/desktop/src/renderer/i18n/ko.json b/packages/i18n/src/locales/ko.json similarity index 100% rename from apps/desktop/src/renderer/i18n/ko.json rename to packages/i18n/src/locales/ko.json diff --git a/apps/desktop/src/renderer/i18n/pt.json b/packages/i18n/src/locales/pt.json similarity index 100% rename from apps/desktop/src/renderer/i18n/pt.json rename to packages/i18n/src/locales/pt.json diff --git a/apps/desktop/src/renderer/i18n/ru.json b/packages/i18n/src/locales/ru.json similarity index 100% rename from apps/desktop/src/renderer/i18n/ru.json rename to packages/i18n/src/locales/ru.json diff --git a/apps/desktop/src/renderer/i18n/th.json b/packages/i18n/src/locales/th.json similarity index 100% rename from apps/desktop/src/renderer/i18n/th.json rename to packages/i18n/src/locales/th.json diff --git a/apps/desktop/src/renderer/i18n/vi.json b/packages/i18n/src/locales/vi.json similarity index 100% rename from apps/desktop/src/renderer/i18n/vi.json rename to packages/i18n/src/locales/vi.json diff --git a/apps/desktop/src/renderer/i18n/zh-TW.json b/packages/i18n/src/locales/zh-TW.json similarity index 100% rename from apps/desktop/src/renderer/i18n/zh-TW.json rename to packages/i18n/src/locales/zh-TW.json diff --git a/apps/desktop/src/renderer/i18n/zh.json b/packages/i18n/src/locales/zh.json similarity index 100% rename from apps/desktop/src/renderer/i18n/zh.json rename to packages/i18n/src/locales/zh.json diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json new file mode 100644 index 0000000..7d02dee --- /dev/null +++ b/packages/i18n/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "jsx": "react-jsx", + "lib": ["ES2022", "DOM"] + }, + "include": ["src/**/*"] +}