feat(V2-1d): packages/i18n 추출 — Electron 독립 i18n 엔진
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 독립성 확보 (monorepo 원칙 준수):
- I18nStorage interface 신설 (load/save 어댑터 추상화)
- window.electronAPI.config 직접 호출 제거
- I18nProvider는 storage prop으로 영속화 어댑터 주입
- 기본값 noopStorage (메모리 한정, 세션 범위)
- 결과: packages/i18n은 React에만 의존, web/mobile에서 재사용 가능
apps/desktop 어댑터:
- App.tsx에 electronI18nStorage 구현 (config.get/set 래핑)
- <I18nProvider storage={electronI18nStorage}>로 주입
apps/desktop 설정:
- package.json: @d3ro/i18n '*' dep 추가
- tsconfig.node/web.json paths에 @d3ro/i18n 추가
- electron.vite.config.ts alias + externalize exclude 추가
- vitest.config.ts alias 추가
일괄 치환 (29 파일):
- ./i18n, ../i18n, ../../i18n → @d3ro/i18n
apps/desktop/src/renderer/i18n/ 디렉토리 완전 제거.
검증: typecheck + build + dev 런타임 모두 통과.
Phase V2-1 전체 완료 (a/b/c/d).
This commit is contained in:
parent
a041f1b6a9
commit
3524e958ba
51 changed files with 175 additions and 66 deletions
|
|
@ -4,10 +4,11 @@ import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
const sharedAlias = {
|
const sharedAlias = {
|
||||||
'@d3ro/core': resolve(__dirname, '../../packages/core/src'),
|
'@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({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
"@d3ro/i18n": "*",
|
||||||
"@d3ro/ui": "*",
|
"@d3ro/ui": "*",
|
||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,23 @@
|
||||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||||
import { ThemeProvider, CssBaseline, useMediaQuery } from '@mui/material'
|
import { ThemeProvider, CssBaseline, useMediaQuery } from '@mui/material'
|
||||||
import { getTheme } from '@d3ro/ui/theme'
|
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 { AppLayout } from './components/AppLayout'
|
||||||
import { UpgradePromptModal } from './components/UpgradePromptModal'
|
import { UpgradePromptModal } from './components/UpgradePromptModal'
|
||||||
import { startSystemAudioCapture, stopSystemAudioCapture } from './utils/systemAudioCapture'
|
import { startSystemAudioCapture, stopSystemAudioCapture } from './utils/systemAudioCapture'
|
||||||
import type { ThemeMode, ConfigChangedEvent } from '@d3ro/core/types'
|
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 {
|
export function App(): React.ReactElement {
|
||||||
const [themeMode, setThemeMode] = useState<ThemeMode>('auto')
|
const [themeMode, setThemeMode] = useState<ThemeMode>('auto')
|
||||||
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
|
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
|
||||||
|
|
@ -60,7 +71,7 @@ export function App(): React.ReactElement {
|
||||||
const theme = useMemo(() => getTheme(themeMode, prefersDark), [themeMode, prefersDark])
|
const theme = useMemo(() => getTheme(themeMode, prefersDark), [themeMode, prefersDark])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider>
|
<I18nProvider storage={electronI18nStorage}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<AppLayout />
|
<AppLayout />
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ import { LicenseModal } from './LicenseModal'
|
||||||
import { OnboardingModal } from './OnboardingModal'
|
import { OnboardingModal } from './OnboardingModal'
|
||||||
import { StatusBar } from './StatusBar'
|
import { StatusBar } from './StatusBar'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type { TranslationKey } from '../i18n'
|
import type { TranslationKey } from '@d3ro/i18n'
|
||||||
import type { LicenseTier } from '@d3ro/core/types'
|
import type { LicenseTier } from '@d3ro/core/types'
|
||||||
|
|
||||||
type Route = 'dashboard' | 'history' | 'dictionary' | 'commands' | 'conversation' | 'knowledge' | 'meeting'
|
type Route = 'dashboard' | 'history' | 'dictionary' | 'commands' | 'conversation' | 'knowledge' | 'meeting'
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import CloseIcon from '@mui/icons-material/Close'
|
||||||
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
||||||
import { MetalCard, PhosphorText, Led } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roTypo } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roTypo } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type {
|
import type {
|
||||||
FileTranscriptionProgress,
|
FileTranscriptionProgress,
|
||||||
FileTranscriptionResult,
|
FileTranscriptionResult,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { d3roPalette } from '@d3ro/ui/theme'
|
import { d3roPalette } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type { HotkeyBinding } from '@d3ro/core/types'
|
import type { HotkeyBinding } from '@d3ro/core/types'
|
||||||
|
|
||||||
// ── 키 이름 매핑 (Windows) ──────────────────────────────
|
// ── 키 이름 매핑 (Windows) ──────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||||
import CancelIcon from '@mui/icons-material/Cancel'
|
import CancelIcon from '@mui/icons-material/Cancel'
|
||||||
import { MetalCard, PhosphorText, Led, ScreenPanel, PhysicalButton } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, ScreenPanel, PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
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'
|
import type { LicenseInfo, LicenseTier, TierComparison, UsageQuota } from '@d3ro/core/types'
|
||||||
|
|
||||||
interface LicenseModalProps {
|
interface LicenseModalProps {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||||
import CancelIcon from '@mui/icons-material/Cancel'
|
import CancelIcon from '@mui/icons-material/Cancel'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type {
|
import type {
|
||||||
LicenseInfo,
|
LicenseInfo,
|
||||||
LicenseTier,
|
LicenseTier,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||||
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { Led } from '@d3ro/ui/components/ds'
|
import { Led } from '@d3ro/ui/components/ds'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
||||||
interface OllamaGuideModalProps {
|
interface OllamaGuideModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { Led } from '@d3ro/ui/components/ds'
|
import { Led } from '@d3ro/ui/components/ds'
|
||||||
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type { HotkeyBinding, AudioDevice } from '@d3ro/core/types'
|
import type { HotkeyBinding, AudioDevice } from '@d3ro/core/types'
|
||||||
|
|
||||||
interface OnboardingModalProps {
|
interface OnboardingModalProps {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Box, Typography } from '@mui/material'
|
||||||
import LockIcon from '@mui/icons-material/Lock'
|
import LockIcon from '@mui/icons-material/Lock'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useProFeature } from '../hooks/useProFeature'
|
import { useProFeature } from '../hooks/useProFeature'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type { Feature } from '@d3ro/core/types'
|
import type { Feature } from '@d3ro/core/types'
|
||||||
|
|
||||||
interface ProBadgeProps {
|
interface ProBadgeProps {
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ import CancelIcon from '@mui/icons-material/Cancel'
|
||||||
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
||||||
import { LicenseTab } from './LicenseTab'
|
import { LicenseTab } from './LicenseTab'
|
||||||
import { useI18n, LOCALE_META } from '../i18n'
|
import { useI18n, LOCALE_META } from '@d3ro/i18n'
|
||||||
import type { Locale } from '../i18n'
|
import type { Locale } from '@d3ro/i18n'
|
||||||
import type { ThemeMode, AppConfig, HotkeyBinding, AudioDevice } from '@d3ro/core/types'
|
import type { ThemeMode, AppConfig, HotkeyBinding, AudioDevice } from '@d3ro/core/types'
|
||||||
import { Feature } from '@d3ro/core/types'
|
import { Feature } from '@d3ro/core/types'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import CloseIcon from '@mui/icons-material/Close'
|
||||||
import { Led } from '@d3ro/ui/components/ds'
|
import { Led } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { OllamaGuideModal } from './OllamaGuideModal'
|
import { OllamaGuideModal } from './OllamaGuideModal'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type { LLMStatus } from '@d3ro/core/types'
|
import type { LLMStatus } from '@d3ro/core/types'
|
||||||
|
|
||||||
export function StatusBar(): React.ReactElement {
|
export function StatusBar(): React.ReactElement {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow'
|
||||||
import { MetalCard, PhosphorText, Led, PhysicalButton } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { PageHeader, EmptyStateCard } from './shared'
|
import { PageHeader, EmptyStateCard } from './shared'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
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'
|
import type { DictationTemplate, TemplateField, TemplateSessionInfo } from '@d3ro/core/types'
|
||||||
|
|
||||||
export function TemplateSection(): React.ReactElement {
|
export function TemplateSection(): React.ReactElement {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import LockIcon from '@mui/icons-material/Lock'
|
import LockIcon from '@mui/icons-material/Lock'
|
||||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'
|
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
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'
|
import type { UpgradePromptEvent, UsageQuota } from '@d3ro/core/types'
|
||||||
|
|
||||||
export function UpgradePromptModal(): React.ReactElement {
|
export function UpgradePromptModal(): React.ReactElement {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { MetalCard } from '@d3ro/ui/components/ds'
|
||||||
import { PhosphorText } from '@d3ro/ui/components/ds'
|
import { PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
||||||
interface TemplateItem {
|
interface TemplateItem {
|
||||||
id: string
|
id: string
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { MarkdownEditor } from './MarkdownEditor'
|
||||||
import { ExportMenu } from './ExportMenu'
|
import { ExportMenu } from './ExportMenu'
|
||||||
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roTypo } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roTypo } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type { MeetingExportFormat } from '@d3ro/core/types'
|
import type { MeetingExportFormat } from '@d3ro/core/types'
|
||||||
|
|
||||||
interface DocumentTabDoc {
|
interface DocumentTabDoc {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import { useState, useRef, useCallback } from 'react'
|
import { useState, useRef, useCallback } from 'react'
|
||||||
import { Box, TextField, Tooltip, Chip } from '@mui/material'
|
import { Box, TextField, Tooltip, Chip } from '@mui/material'
|
||||||
import { d3roPalette, d3roFontMono } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
||||||
// Phase 15.5: 화자별 색상 매핑 (d3roPalette SSOT)
|
// Phase 15.5: 화자별 색상 매핑 (d3roPalette SSOT)
|
||||||
const SPEAKER_COLORS = [
|
const SPEAKER_COLORS = [
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Menu, MenuItem } from '@mui/material'
|
||||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
||||||
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
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'
|
import type { MeetingExportFormat } from '@d3ro/core/types'
|
||||||
|
|
||||||
interface ExportMenuProps {
|
interface ExportMenuProps {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Box } from '@mui/material'
|
||||||
import { MarkdownRenderer } from './MarkdownRenderer'
|
import { MarkdownRenderer } from './MarkdownRenderer'
|
||||||
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
||||||
interface MarkdownEditorProps {
|
interface MarkdownEditorProps {
|
||||||
content: string
|
content: string
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||||
import { PhosphorText } from '@d3ro/ui/components/ds'
|
import { PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow } from '@d3ro/ui/theme'
|
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'
|
import type { MeetingChatMessage } from '@d3ro/core/types'
|
||||||
|
|
||||||
interface MeetingChatPanelProps {
|
interface MeetingChatPanelProps {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { AddDocumentDialog } from './AddDocumentDialog'
|
||||||
import { MeetingChatPanel } from './MeetingChatPanel'
|
import { MeetingChatPanel } from './MeetingChatPanel'
|
||||||
import { PhosphorText } from '@d3ro/ui/components/ds'
|
import { PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type {
|
import type {
|
||||||
MeetingSessionDetail,
|
MeetingSessionDetail,
|
||||||
MeetingDocument,
|
MeetingDocument,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { EditableSegment } from './EditableSegment'
|
||||||
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { PhosphorText } from '@d3ro/ui/components/ds'
|
import { PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
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 FileDownloadIcon from '@mui/icons-material/FileDownload'
|
||||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
|
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
|
||||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver'
|
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver'
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import SummarizeIcon from '@mui/icons-material/Summarize'
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||||
import { MetalCard, Led } from '@d3ro/ui/components/ds'
|
import { MetalCard, Led } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import { formatDuration } from '../../utils/formatters'
|
import { formatDuration } from '../../utils/formatters'
|
||||||
import type { HistoryEntry, MemoTag, MeetingSummaryResult } from '@d3ro/core/types'
|
import type { HistoryEntry, MemoTag, MeetingSummaryResult } from '@d3ro/core/types'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import LinkIcon from '@mui/icons-material/Link'
|
||||||
import { MetalCard, PhosphorText, Led, ScreenPanel } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, ScreenPanel } from '@d3ro/ui/components/ds'
|
||||||
import { PageHeader, EmptyStateCard } from '../components/shared'
|
import { PageHeader, EmptyStateCard } from '../components/shared'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import { TemplateSection } from '../components/TemplateSection'
|
import { TemplateSection } from '../components/TemplateSection'
|
||||||
import type { VoiceCommandRule, VoiceCommandKeyword, KeywordMatchMode, LLMChain, ChainStep } from '@d3ro/core/types'
|
import type { VoiceCommandRule, VoiceCommandKeyword, KeywordMatchMode, LLMChain, ChainStep } from '@d3ro/core/types'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Box } from '@mui/material'
|
||||||
import { CrtDisplay, InstrumentPanel, Led, MetalCard, PhosphorText, ScreenPanel, ButtonGroup, PhysicalButton } from '@d3ro/ui/components/ds'
|
import { CrtDisplay, InstrumentPanel, Led, MetalCard, PhosphorText, ScreenPanel, ButtonGroup, PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { EmptyStateCard, HistoryEntryCard } from '../components/shared'
|
import { EmptyStateCard, HistoryEntryCard } from '../components/shared'
|
||||||
import { d3roPalette, d3roTypo } from '@d3ro/ui/theme'
|
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 { formatRecordingTime, formatRecordingTimeUnit, formatNumber, getDateKey } from '../utils/formatters'
|
||||||
import { FileDropZone } from '../components/FileDropZone'
|
import { FileDropZone } from '../components/FileDropZone'
|
||||||
import type { StatsSummary, HistoryEntry, HotkeyBinding, CaptionState, LicenseTier, UsageQuota } from '@d3ro/core/types'
|
import type { StatsSummary, HistoryEntry, HotkeyBinding, CaptionState, LicenseTier, UsageQuota } from '@d3ro/core/types'
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import EditIcon from '@mui/icons-material/Edit'
|
||||||
import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { PageHeader, SearchInput, EmptyStateCard } from '../components/shared'
|
import { PageHeader, SearchInput, EmptyStateCard } from '../components/shared'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
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'
|
import type { DictionaryEntry, DictionaryPage as DictPageData } from '@d3ro/core/types'
|
||||||
|
|
||||||
const PAGE_SIZE = 50
|
const PAGE_SIZE = 50
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Box, Chip, IconButton, Tooltip } from '@mui/material'
|
||||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
||||||
import { PhosphorText } from '@d3ro/ui/components/ds'
|
import { PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import { getDateKey } from '../utils/formatters'
|
import { getDateKey } from '../utils/formatters'
|
||||||
import { EmptyStateCard, SearchInput, PageHeader, HistoryEntryCard } from '../components/shared'
|
import { EmptyStateCard, SearchInput, PageHeader, HistoryEntryCard } from '../components/shared'
|
||||||
import type { HistoryEntry, HistoryPage as HistoryPageData, TagCount } from '@d3ro/core/types'
|
import type { HistoryEntry, HistoryPage as HistoryPageData, TagCount } from '@d3ro/core/types'
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import DescriptionIcon from '@mui/icons-material/Description'
|
||||||
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '@d3ro/ui/components/ds'
|
||||||
import { PageHeader, EmptyStateCard } from '../components/shared'
|
import { PageHeader, EmptyStateCard } from '../components/shared'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
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'
|
import type { RAGDocument, RAGQueryResult, RAGIndexProgress } from '@d3ro/core/types'
|
||||||
|
|
||||||
export function KnowledgeBasePage(): React.ReactElement {
|
export function KnowledgeBasePage(): React.ReactElement {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { MeetingDetailTabs } from '../components/meeting/MeetingDetailTabs'
|
||||||
import { EditableSegment } from '../components/meeting/EditableSegment'
|
import { EditableSegment } from '../components/meeting/EditableSegment'
|
||||||
import { PageHeader, EmptyStateCard } from '../components/shared'
|
import { PageHeader, EmptyStateCard } from '../components/shared'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type {
|
import type {
|
||||||
MeetingSessionSummary,
|
MeetingSessionSummary,
|
||||||
MeetingSessionDetail,
|
MeetingSessionDetail,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import CancelIcon from '@mui/icons-material/Cancel'
|
||||||
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel, InstrumentPanel } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel, InstrumentPanel } from '@d3ro/ui/components/ds'
|
||||||
import { PageHeader } from '../components/shared'
|
import { PageHeader } from '../components/shared'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type {
|
import type {
|
||||||
ConversationState,
|
ConversationState,
|
||||||
ConversationMessage,
|
ConversationMessage,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
"@d3ro/core": ["../../packages/core/src/index.ts"],
|
"@d3ro/core": ["../../packages/core/src/index.ts"],
|
||||||
"@d3ro/core/*": ["../../packages/core/src/*"],
|
"@d3ro/core/*": ["../../packages/core/src/*"],
|
||||||
"@d3ro/ui": ["../../packages/ui/src/index.ts"],
|
"@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,
|
"strict": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
"@d3ro/core": ["../../packages/core/src/index.ts"],
|
"@d3ro/core": ["../../packages/core/src/index.ts"],
|
||||||
"@d3ro/core/*": ["../../packages/core/src/*"],
|
"@d3ro/core/*": ["../../packages/core/src/*"],
|
||||||
"@d3ro/ui": ["../../packages/ui/src/index.ts"],
|
"@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",
|
"jsx": "react-jsx",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@d3ro/core': resolve(__dirname, '../../packages/core/src'),
|
'@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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@
|
||||||
## 현재 단계
|
## 현재 단계
|
||||||
|
|
||||||
**V1 (Electron) 완료** → **V2 (Monorepo) 진행 중**
|
**V1 (Electron) 완료** → **V2 (Monorepo) 진행 중**
|
||||||
- Phase V2-1a ✅ 완료 (Monorepo 구조 이동)
|
- Phase V2-1 (전체) ✅ 완료
|
||||||
- Phase V2-1b ✅ 완료 (packages/core 추출)
|
- V2-1a: Monorepo 구조 이동
|
||||||
- Phase V2-1c ✅ 완료 (packages/ui 추출)
|
- V2-1b: packages/core 추출
|
||||||
- 다음: Phase V2-1d (packages/i18n 추출)
|
- V2-1c: packages/ui 추출
|
||||||
|
- V2-1d: packages/i18n 추출
|
||||||
|
- 다음: Phase V2-2 (Supabase 인프라)
|
||||||
|
|
||||||
## V1 완료 페이즈
|
## V1 완료 페이즈
|
||||||
|
|
||||||
|
|
@ -40,7 +42,28 @@
|
||||||
| V2-1a | npm workspaces + apps/desktop으로 V1 이동 | ✅ 완료 |
|
| V2-1a | npm workspaces + apps/desktop으로 V1 이동 | ✅ 완료 |
|
||||||
| V2-1b | packages/core 추출 (types, errors, ipc-channels, constants, utils) | ✅ 완료 |
|
| V2-1b | packages/core 추출 (types, errors, ipc-channels, constants, utils) | ✅ 완료 |
|
||||||
| V2-1c | packages/ui 추출 (DS 컴포넌트 + theme + theme-vars) | ✅ 완료 |
|
| 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` 어댑터 구현 후 `<I18nProvider storage={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 완료 내역**
|
**V2-1c 완료 내역**
|
||||||
- `packages/ui/` (@d3ro/ui) 신규 생성:
|
- `packages/ui/` (@d3ro/ui) 신규 생성:
|
||||||
|
|
|
||||||
16
package-lock.json
generated
16
package-lock.json
generated
|
|
@ -27,6 +27,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@d3ro/core": "*",
|
"@d3ro/core": "*",
|
||||||
|
"@d3ro/i18n": "*",
|
||||||
"@d3ro/ui": "*",
|
"@d3ro/ui": "*",
|
||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
|
|
@ -400,6 +401,10 @@
|
||||||
"resolved": "apps/desktop",
|
"resolved": "apps/desktop",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@d3ro/i18n": {
|
||||||
|
"resolved": "packages/i18n",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@d3ro/ui": {
|
"node_modules/@d3ro/ui": {
|
||||||
"resolved": "packages/ui",
|
"resolved": "packages/ui",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -12820,6 +12825,17 @@
|
||||||
"@types/node": "^22.13.0"
|
"@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": {
|
"packages/ui": {
|
||||||
"name": "@d3ro/ui",
|
"name": "@d3ro/ui",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
|
||||||
21
packages/i18n/package.json
Normal file
21
packages/i18n/package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
// src/renderer/i18n/index.ts
|
// packages/i18n/src/index.tsx
|
||||||
// SSOT i18n 엔진: 타입 안전 키, React Context, fallback 체인, Intl 포맷팅
|
// SSOT i18n 엔진: 타입 안전 키, React Context, fallback 체인, Intl 포맷팅
|
||||||
// 마스터: ko.json — 모든 키의 단일 소스
|
// 마스터: ko.json — 모든 키의 단일 소스
|
||||||
|
// Electron 독립: 로케일 영속화는 I18nStorage prop으로 주입받음.
|
||||||
|
|
||||||
import { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react'
|
import { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import ko from './ko.json'
|
import ko from './locales/ko.json'
|
||||||
import en from './en.json'
|
import en from './locales/en.json'
|
||||||
import ja from './ja.json'
|
import ja from './locales/ja.json'
|
||||||
import zh from './zh.json'
|
import zh from './locales/zh.json'
|
||||||
import zhTW from './zh-TW.json'
|
import zhTW from './locales/zh-TW.json'
|
||||||
import es from './es.json'
|
import es from './locales/es.json'
|
||||||
import fr from './fr.json'
|
import fr from './locales/fr.json'
|
||||||
import de from './de.json'
|
import de from './locales/de.json'
|
||||||
import pt from './pt.json'
|
import pt from './locales/pt.json'
|
||||||
import ru from './ru.json'
|
import ru from './locales/ru.json'
|
||||||
import vi from './vi.json'
|
import vi from './locales/vi.json'
|
||||||
import th from './th.json'
|
import th from './locales/th.json'
|
||||||
|
|
||||||
// ── 타입 ────────────────────────────────────────────────
|
// ── 타입 ────────────────────────────────────────────────
|
||||||
/** 마스터 키에서 자동 추출된 번역 키 유니온 */
|
/** 마스터 키에서 자동 추출된 번역 키 유니온 */
|
||||||
|
|
@ -147,29 +148,52 @@ export interface I18nContextValue {
|
||||||
|
|
||||||
const I18nContext = createContext<I18nContextValue | null>(null)
|
const I18nContext = createContext<I18nContextValue | null>(null)
|
||||||
|
|
||||||
|
// ── Storage 추상화 ────────────────────────────────────
|
||||||
|
/**
|
||||||
|
* 로케일 영속화 어댑터. 앱별로 주입 (Electron config, localStorage, etc).
|
||||||
|
* 기본 no-op: 메모리 기반으로만 동작.
|
||||||
|
*/
|
||||||
|
export interface I18nStorage {
|
||||||
|
load: () => Promise<string | null> | string | null
|
||||||
|
save: (locale: Locale) => void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const noopStorage: I18nStorage = {
|
||||||
|
load: () => null,
|
||||||
|
save: () => {}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Provider ───────────────────────────────────────────
|
// ── Provider ───────────────────────────────────────────
|
||||||
export interface I18nProviderProps {
|
export interface I18nProviderProps {
|
||||||
initialLocale?: Locale
|
initialLocale?: Locale
|
||||||
|
/** 로케일 영속화 어댑터. 없으면 메모리 기반(세션 한정). */
|
||||||
|
storage?: I18nStorage
|
||||||
children: ReactNode
|
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<Locale>(initialLocale)
|
const [locale, setLocaleState] = useState<Locale>(initialLocale)
|
||||||
|
|
||||||
// ConfigService에서 저장된 언어 로드
|
// storage에서 저장된 언어 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electronAPI.config.get({ key: 'language' }).then((r) => {
|
Promise.resolve(storage.load()).then((loaded) => {
|
||||||
if (r.success && r.data && isValidLocale(r.data as string)) {
|
if (loaded && isValidLocale(loaded)) {
|
||||||
setLocaleState(r.data as Locale)
|
setLocaleState(loaded)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [])
|
}, [storage])
|
||||||
|
|
||||||
const setLocale = useCallback((newLocale: Locale) => {
|
const setLocale = useCallback(
|
||||||
|
(newLocale: Locale) => {
|
||||||
setLocaleState(newLocale)
|
setLocaleState(newLocale)
|
||||||
// 설정에 저장
|
void storage.save(newLocale)
|
||||||
window.electronAPI.config.set({ key: 'language', value: newLocale })
|
},
|
||||||
}, [])
|
[storage]
|
||||||
|
)
|
||||||
|
|
||||||
const value = useMemo((): I18nContextValue => {
|
const value = useMemo((): I18nContextValue => {
|
||||||
const translations = LOCALE_MAP[locale] ?? ko
|
const translations = LOCALE_MAP[locale] ?? ko
|
||||||
9
packages/i18n/tsconfig.json
Normal file
9
packages/i18n/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ES2022", "DOM"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue