From 36018196b03ea584a04edd22b8db2429dfc4ea06 Mon Sep 17 00:00:00 2001 From: holole Date: Sat, 14 Feb 2026 17:58:26 +0900 Subject: [PATCH 1/6] add i18n infrastructure --- frontend/package-lock.json | 105 +++++++++++++++++++++++-- frontend/package.json | 2 + frontend/src/i18n/index.ts | 21 +++++ frontend/src/i18n/locales/en/common.ts | 11 +++ frontend/src/i18n/locales/ko/common.ts | 11 +++ frontend/src/main.tsx | 1 + 6 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 frontend/src/i18n/index.ts create mode 100644 frontend/src/i18n/locales/en/common.ts create mode 100644 frontend/src/i18n/locales/ko/common.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 74c3d16..010a55f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,20 +1,22 @@ { - "name": "frontend", + "name": "lyra-frontend", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "frontend", + "name": "lyra-frontend", "version": "0.0.0", "dependencies": { "@monaco-editor/react": "^4.7.0", "axios": "^1.13.4", "clsx": "^2.1.1", "framer-motion": "^12.29.2", + "i18next": "^25.8.7", "lucide-react": "^0.563.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-i18next": "^16.5.4", "react-router-dom": "^7.13.0", "tailwind-merge": "^3.4.0", "xterm": "^5.3.0", @@ -287,6 +289,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -3140,6 +3151,47 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.8.7", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.7.tgz", + "integrity": "sha512-ttxxc5+67S/0hhoeVdEgc1lRklZhdfcUSEPp1//uUG2NB88X3667gRsDar+ZWQFdysnOsnb32bcoMsa4mtzhkQ==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3213,7 +3265,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -3315,7 +3366,6 @@ "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "dev": true, "license": "MPL-2.0", - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -3934,6 +3984,33 @@ "react": "^19.2.4" } }, + "node_modules/react-i18next": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz", + "integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -4208,7 +4285,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -4291,6 +4368,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -4367,6 +4453,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6f8a71a..b02fb65 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,9 +14,11 @@ "axios": "^1.13.4", "clsx": "^2.1.1", "framer-motion": "^12.29.2", + "i18next": "^25.8.7", "lucide-react": "^0.563.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-i18next": "^16.5.4", "react-router-dom": "^7.13.0", "tailwind-merge": "^3.4.0", "xterm": "^5.3.0", diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts new file mode 100644 index 0000000..39e8b09 --- /dev/null +++ b/frontend/src/i18n/index.ts @@ -0,0 +1,21 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import enCommon from './locales/en/common'; +import koCommon from './locales/ko/common'; + +void i18n.use(initReactI18next).init({ + resources: { + en: { common: enCommon }, + ko: { common: koCommon }, + }, + supportedLngs: ['en', 'ko'], + lng: 'en', + fallbackLng: 'en', + ns: ['common'], + defaultNS: 'common', + interpolation: { + escapeValue: false, + }, +}); + +export default i18n; diff --git a/frontend/src/i18n/locales/en/common.ts b/frontend/src/i18n/locales/en/common.ts new file mode 100644 index 0000000..bb62c11 --- /dev/null +++ b/frontend/src/i18n/locales/en/common.ts @@ -0,0 +1,11 @@ +const enCommon = { + app: { + name: 'Lyra', + }, + common: { + save: 'Save', + cancel: 'Cancel', + }, +} as const; + +export default enCommon; diff --git a/frontend/src/i18n/locales/ko/common.ts b/frontend/src/i18n/locales/ko/common.ts new file mode 100644 index 0000000..6da890d --- /dev/null +++ b/frontend/src/i18n/locales/ko/common.ts @@ -0,0 +1,11 @@ +const koCommon = { + app: { + name: 'Lyra', + }, + common: { + save: '저장', + cancel: '취소', + }, +} as const; + +export default koCommon; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 1c3a726..1e69233 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,6 +2,7 @@ import axios from 'axios' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App.tsx' +import './i18n' import './index.css' axios.defaults.baseURL = '/api'; From 7f06ba5fccb90da171b92ba8d64619e470d5ec79 Mon Sep 17 00:00:00 2001 From: holole Date: Sat, 14 Feb 2026 18:09:40 +0900 Subject: [PATCH 2/6] extract shared, common strings --- frontend/src/components/Modal.tsx | 6 +- frontend/src/components/Sidebar.tsx | 16 ++--- frontend/src/i18n/locales/en/common.ts | 74 ++++++++++++++++++++++- frontend/src/i18n/locales/ko/common.ts | 73 ++++++++++++++++++++++- frontend/src/pages/Dashboard.tsx | 82 ++++++++++++-------------- frontend/src/pages/Settings.tsx | 26 ++++---- 6 files changed, 211 insertions(+), 66 deletions(-) diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index 2a1c4de..3c9d163 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -1,4 +1,5 @@ import { X } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; interface ModalProps { isOpen: boolean; @@ -19,6 +20,7 @@ export default function Modal({ type = 'confirm', isDestructive = false }: ModalProps) { + const { t } = useTranslation(); if (!isOpen) return null; return ( @@ -39,7 +41,7 @@ export default function Modal({ onClick={onClose} className="px-4 py-2 text-sm font-medium text-gray-300 hover:text-white bg-[#3f3f46] hover:bg-[#52525b] rounded-lg transition-colors" > - Cancel + {t('actions.cancel')} )} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index cc2ed9e..71d7c76 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,19 +1,21 @@ import clsx from 'clsx'; import { FileCode, LayoutDashboard, PlusCircle, Settings, Terminal } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { Link, useLocation } from 'react-router-dom'; import { useApp } from '../context/AppContext'; const navItems = [ - { name: 'Dashboard', path: '/', icon: LayoutDashboard }, - { name: 'Provisioning', path: '/provisioning', icon: PlusCircle }, - { name: 'Terminal', path: '/terminal', icon: Terminal }, - { name: 'Templates', path: '/templates', icon: FileCode }, - { name: 'Settings', path: '/settings', icon: Settings }, + { nameKey: 'nav.dashboard', path: '/', icon: LayoutDashboard }, + { nameKey: 'nav.provisioning', path: '/provisioning', icon: PlusCircle }, + { nameKey: 'nav.terminal', path: '/terminal', icon: Terminal }, + { nameKey: 'nav.templates', path: '/templates', icon: FileCode }, + { nameKey: 'nav.settings', path: '/settings', icon: Settings }, ]; export default function Sidebar() { const location = useLocation(); const { appName } = useApp(); + const { t } = useTranslation(); return (
@@ -37,7 +39,7 @@ export default function Sidebar() { )} > - {item.name} + {t(item.nameKey)} ); })} @@ -45,7 +47,7 @@ export default function Sidebar() {
- System Status: Online + {t('system.status')}: {t('system.online')}
diff --git a/frontend/src/i18n/locales/en/common.ts b/frontend/src/i18n/locales/en/common.ts index bb62c11..4eb665d 100644 --- a/frontend/src/i18n/locales/en/common.ts +++ b/frontend/src/i18n/locales/en/common.ts @@ -2,9 +2,81 @@ const enCommon = { app: { name: 'Lyra', }, - common: { + nav: { + dashboard: 'Dashboard', + provisioning: 'Provisioning', + terminal: 'Terminal', + templates: 'Templates', + settings: 'Settings', + }, + system: { + status: 'System Status', + online: 'Online', + }, + actions: { save: 'Save', cancel: 'Cancel', + confirm: 'Confirm', + ok: 'OK', + close: 'Close', + refresh: 'Refresh', + start: 'Start', + stop: 'Stop', + delete: 'Delete', + selectFile: 'Select File', + reset: 'Reset', + testConnection: 'Test Connection', + }, + labels: { + name: 'Name', + status: 'Status', + actions: 'Actions', + gpu: 'GPU', + access: 'Access', + instances: 'Instances', + }, + status: { + loading: 'Loading...', + starting: 'Starting', + stopping: 'Stopping', + success: 'Success', + error: 'Error', + }, + messages: { + noData: 'No data found.', + noEnvironments: 'No environments found.', + loadingEnvironments: 'Loading environments...', + notAvailable: 'Not available', + }, + resource: { + cleanupImages: 'Cleanup Images', + removeSelectedVolumes: 'Remove Selected Volumes', + cleanupBuildCache: 'Cleanup Build Cache', + }, + dashboard: { + title: 'Dashboard', + subtitle: 'Manage your GPU virtual environments', + deleteEnvironmentTitle: 'Delete Environment', + deleteEnvironmentMessage: + 'Are you sure you want to delete this environment? This action cannot be undone and will permanently remove the container and data.', + volumeMounts: 'Volume Mounts', + mountedVolumesFor: 'Mounted volumes for {{name}}', + customPortMappings: 'Custom Port Mappings', + customPortsFor: 'Custom ports for {{name}}', + containerErrorLog: 'Container Error Log', + last50LinesFor: 'Last 50 lines of logs for {{name}}', + noLogsAvailable: 'No logs available.', + viewErrorLogs: 'View Error Logs', + stopInstance: 'Stop Instance', + startInstance: 'Start Instance', + viewVolumes: 'View Volumes', + noVolumes: 'No Volumes', + viewCustomPorts: 'View Custom Ports', + noCustomPorts: 'No Custom Ports', + copySshCommand: 'Copy SSH command (port: {{port}})', + environmentMustBeRunning: 'Environment must be running (port: {{port}})', + openJupyterLab: 'Open Jupyter Lab', + openCodeServer: 'Open code-server', }, } as const; diff --git a/frontend/src/i18n/locales/ko/common.ts b/frontend/src/i18n/locales/ko/common.ts index 6da890d..f049736 100644 --- a/frontend/src/i18n/locales/ko/common.ts +++ b/frontend/src/i18n/locales/ko/common.ts @@ -2,9 +2,80 @@ const koCommon = { app: { name: 'Lyra', }, - common: { + nav: { + dashboard: '대시보드', + provisioning: '프로비저닝', + terminal: '터미널', + templates: '템플릿', + settings: '설정', + }, + system: { + status: '시스템 상태', + online: '온라인', + }, + actions: { save: '저장', cancel: '취소', + confirm: '확인', + ok: '확인', + close: '닫기', + refresh: '새로고침', + start: '시작', + stop: '중지', + delete: '삭제', + selectFile: '파일 선택', + reset: '초기화', + testConnection: '연결 테스트', + }, + labels: { + name: '이름', + status: '상태', + actions: '동작', + gpu: 'GPU', + access: '접속', + instances: '인스턴스', + }, + status: { + loading: '로딩 중...', + starting: '시작 중', + stopping: '중지 중', + success: '성공', + error: '오류', + }, + messages: { + noData: '데이터가 없습니다.', + noEnvironments: '환경이 없습니다.', + loadingEnvironments: '환경 정보를 불러오는 중...', + notAvailable: '사용 불가', + }, + resource: { + cleanupImages: '이미지 정리', + removeSelectedVolumes: '선택한 볼륨 제거', + cleanupBuildCache: '빌드 캐시 정리', + }, + dashboard: { + title: '대시보드', + subtitle: 'GPU 가상 환경을 관리합니다', + deleteEnvironmentTitle: '환경 삭제', + deleteEnvironmentMessage: '이 환경을 삭제하시겠습니까? 이 작업은 되돌릴 수 없으며 컨테이너와 데이터가 영구 삭제됩니다.', + volumeMounts: '볼륨 마운트', + mountedVolumesFor: '{{name}}의 마운트 볼륨', + customPortMappings: '커스텀 포트 매핑', + customPortsFor: '{{name}}의 커스텀 포트', + containerErrorLog: '컨테이너 오류 로그', + last50LinesFor: '{{name}}의 최근 50줄 로그', + noLogsAvailable: '로그가 없습니다.', + viewErrorLogs: '오류 로그 보기', + stopInstance: '인스턴스 중지', + startInstance: '인스턴스 시작', + viewVolumes: '볼륨 보기', + noVolumes: '볼륨 없음', + viewCustomPorts: '커스텀 포트 보기', + noCustomPorts: '커스텀 포트 없음', + copySshCommand: 'SSH 명령 복사 (포트: {{port}})', + environmentMustBeRunning: '환경이 실행 중이어야 합니다 (포트: {{port}})', + openJupyterLab: 'Jupyter Lab 열기', + openCodeServer: 'code-server 열기', }, } as const; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index a31d91d..f184d6f 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,6 +1,7 @@ import axios from 'axios'; import { Code2, HardDrive, HelpCircle, LayoutTemplate, Network, Play, RefreshCw, Square, SquareTerminal, Trash2, X } from 'lucide-react'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import Modal from '../components/Modal'; import { useToast } from '../context/ToastContext'; @@ -34,6 +35,7 @@ const ENVS_CACHE_KEY = 'lyra.dashboard.environments'; export default function Dashboard() { const { showToast } = useToast(); + const { t } = useTranslation(); const [environments, setEnvironments] = useState(() => { try { if (typeof window === 'undefined') return []; @@ -86,18 +88,18 @@ export default function Dashboard() { } }; - const fetchErrorLogs = async (envId: string) => { + const fetchErrorLogs = useCallback(async (envId: string) => { try { setLogLoading(true); const res = await axios.get(`environments/${envId}/logs`); setErrorLog(res.data.logs); } catch (error) { console.error("Failed to fetch logs", error); - setErrorLog("Failed to fetch logs."); + setErrorLog(t('status.error')); } finally { setLogLoading(false); } - }; + }, [t]); useEffect(() => { if (errorLogEnv) { @@ -105,7 +107,7 @@ export default function Dashboard() { } else { setErrorLog(""); } - }, [errorLogEnv]); + }, [errorLogEnv, fetchErrorLogs]); const deleteEnvironment = async () => { if (!deleteId) return; @@ -195,8 +197,8 @@ export default function Dashboard() { isOpen={!!deleteId} onClose={() => setDeleteId(null)} onConfirm={deleteEnvironment} - title="Delete Environment" - message="Are you sure you want to delete this environment? This action cannot be undone and will permanently remove the container and data." + title={t('dashboard.deleteEnvironmentTitle')} + message={t('dashboard.deleteEnvironmentMessage')} isDestructive={true} /> @@ -207,16 +209,14 @@ export default function Dashboard() {

- Volume Mounts + {t('dashboard.volumeMounts')}

-

- Mounted volumes for {selectedVolEnv.name} -

+

{t('dashboard.mountedVolumesFor', { name: selectedVolEnv.name })}

{selectedVolEnv.mount_config.map((mount, idx) => (
@@ -240,7 +240,7 @@ export default function Dashboard() { onClick={() => setSelectedVolEnv(null)} className="px-4 py-2 rounded-lg text-sm font-medium bg-[#3f3f46] hover:bg-[#52525b] text-white transition-all" > - Close + {t('actions.close')}
@@ -254,16 +254,14 @@ export default function Dashboard() {

- Custom Port Mappings + {t('dashboard.customPortMappings')}

-

- Custom ports for {selectedPortEnv.name} -

+

{t('dashboard.customPortsFor', { name: selectedPortEnv.name })}

{selectedPortEnv.custom_ports.map((mapping, idx) => (
@@ -283,7 +281,7 @@ export default function Dashboard() { onClick={() => setSelectedPortEnv(null)} className="px-4 py-2 rounded-lg text-sm font-medium bg-[#3f3f46] hover:bg-[#52525b] text-white transition-all" > - Close + {t('actions.close')}
@@ -297,16 +295,14 @@ export default function Dashboard() {

- Container Error Log + {t('dashboard.containerErrorLog')}

-

- Last 50 lines of logs for {errorLogEnv.name} -

+

{t('dashboard.last50LinesFor', { name: errorLogEnv.name })}

{logLoading ? (
@@ -314,7 +310,7 @@ export default function Dashboard() {
) : (
-                                {errorLog || "No logs available."}
+                                {errorLog || t('dashboard.noLogsAvailable')}
                             
)}
@@ -324,7 +320,7 @@ export default function Dashboard() { onClick={() => setErrorLogEnv(null)} className="px-4 py-2 rounded-lg text-sm font-medium bg-[#3f3f46] hover:bg-[#52525b] text-white transition-all" > - Close + {t('actions.close')}
@@ -333,15 +329,15 @@ export default function Dashboard() {
-

Dashboard

-

Manage your GPU virtual environments

+

{t('dashboard.title')}

+

{t('dashboard.subtitle')}

-

Instances

+

{t('labels.instances')}

{!hasLoadedOnce && loading ? ( -
Loading environments...
+
{t('messages.loadingEnvironments')}
) : environments.length === 0 ? ( -
No environments found.
+
{t('messages.noEnvironments')}
) : ( - - - - - + + + + + @@ -391,14 +387,14 @@ export default function Dashboard() { 'bg-red-500/10 text-red-500' }`}> {actionLoading[env.id] - ? (env.status === 'running' ? "Stopping" : "Starting") + ? (env.status === 'running' ? t('status.stopping') : t('status.starting')) : (env.status.charAt(0).toUpperCase() + env.status.slice(1))} {env.status === 'error' && ( @@ -420,8 +416,8 @@ export default function Dashboard() {
{env.status === 'running' - ? `Copy SSH command (port: ${env.ssh_port})` - : `Environment must be running (port: ${env.ssh_port})`} + ? t('dashboard.copySshCommand', { port: env.ssh_port }) + : t('dashboard.environmentMustBeRunning', { port: env.ssh_port })}
/ @@ -433,7 +429,7 @@ export default function Dashboard() {
- Open Jupyter Lab + {t('dashboard.openJupyterLab')}
/ @@ -445,7 +441,7 @@ export default function Dashboard() {
- Open code-server + {t('dashboard.openCodeServer')}
@@ -473,7 +469,7 @@ export default function Dashboard() { ? "hover:bg-[#3f3f46] text-gray-400 hover:text-yellow-400" : "hover:bg-[#3f3f46] text-gray-400 hover:text-green-400" } ${actionLoading[env.id] ? "animate-pulse opacity-80" : ""}`} - title={isRunning ? "Stop Instance" : "Start Instance"} + title={isRunning ? t('dashboard.stopInstance') : t('dashboard.startInstance')} > {actionLoading[env.id] || isTransitioning ? @@ -496,7 +492,7 @@ export default function Dashboard() { ? "text-gray-400 hover:text-blue-400 hover:bg-blue-500/10" : "text-gray-600 cursor-not-allowed opacity-30" }`} - title={env.mount_config && env.mount_config.length > 0 ? "View Volumes" : "No Volumes"} + title={env.mount_config && env.mount_config.length > 0 ? t('dashboard.viewVolumes') : t('dashboard.noVolumes')} > @@ -512,14 +508,14 @@ export default function Dashboard() { ? "text-gray-400 hover:text-cyan-400 hover:bg-cyan-500/10" : "text-gray-600 cursor-not-allowed opacity-30" }`} - title={env.custom_ports && env.custom_ports.length > 0 ? "View Custom Ports" : "No Custom Ports"} + title={env.custom_ports && env.custom_ports.length > 0 ? t('dashboard.viewCustomPorts') : t('dashboard.noCustomPorts')} > diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index cd16970..b2d9403 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1,6 +1,7 @@ import axios from 'axios'; import { AlertCircle, CheckCircle2, FolderOpen, HardDrive, ImageIcon, Key, Lock, RefreshCw, Save, Server, Trash2 } from 'lucide-react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useApp } from '../context/AppContext'; import { encrypt } from '../utils/crypto'; @@ -8,6 +9,7 @@ type StatusState = { type: 'idle' | 'loading' | 'success' | 'error'; message?: s export default function Settings() { const { appName, setAppName, faviconDataUrl, setFavicon, isLoading: appLoading } = useApp(); + const { t } = useTranslation(); const [localAppName, setLocalAppName] = useState(appName); const [localFaviconDataUrl, setLocalFaviconDataUrl] = useState(faviconDataUrl); const [appNameStatus, setAppNameStatus] = useState({ type: 'idle' }); @@ -359,7 +361,7 @@ export default function Settings() { disabled={isLoading} className="flex-1 bg-[#18181b] border border-[#3f3f46] rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-blue-500 transition-all" /> - + @@ -386,7 +388,7 @@ export default function Settings() { onClick={() => faviconInputRef.current?.click()} className="bg-[#3f3f46] hover:bg-[#52525b] text-white px-4 py-2 rounded-lg text-sm font-medium transition-all" > - Select File + {t('actions.selectFile')} @@ -484,7 +486,7 @@ export default function Settings() { @@ -514,7 +516,7 @@ export default function Settings() { onClick={handleTestSsh} className="w-full sm:w-auto bg-[#3f3f46] hover:bg-[#52525b] text-white px-6 py-2.5 rounded-lg font-medium transition-all" > - Test Connection + {t('actions.testConnection')} @@ -544,14 +546,14 @@ export default function Settings() { className="self-start sm:self-auto bg-[#3f3f46] hover:bg-[#52525b] text-white px-3 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-all" > - Refresh + {t('actions.refresh')}
-

Unused Images

+

Unused Images

{ @@ -263,7 +265,7 @@ export default function Provisioning() { ? "border-red-500/50 focus:border-red-500 focus:ring-red-500/20" : "border-[#3f3f46] focus:border-blue-500 focus:ring-blue-500" )} - placeholder="My Template" + placeholder={t('provisioning.templateNamePlaceholder')} /> {templateErrors.name && (

{templateErrors.name}

@@ -271,12 +273,12 @@ export default function Provisioning() {
- +
NameStatusAccessGPUActions{t('labels.name')}{t('labels.status')}{t('labels.access')}{t('labels.gpu')}{t('labels.actions')}