From 4a35fb51e5fef201bed57acdb7f74ee10a30f51d Mon Sep 17 00:00:00 2001 From: Dayum_gud Date: Sat, 9 May 2026 14:08:57 +0900 Subject: [PATCH] feat(i18n): add Korean (ko) locale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add web/src/i18n/ko.ts with full Korean translations for all UI strings - Extend Locale type to include 'ko' in types.ts - Register ko translations in context.tsx (TRANSLATIONS map + locale guard) - Update LanguageSwitcher to cycle EN → 中文 → 한국어 → EN with 🇰🇷 flag All 15 translation namespaces covered: common, app, status, sessions, analytics, models, logs, cron, profiles, pluginsPage, skills, config, env, oauth, language, theme --- web/src/components/LanguageSwitcher.tsx | 21 +- web/src/i18n/context.tsx | 5 +- web/src/i18n/ko.ts | 429 ++++++++++++++++++++++++ web/src/i18n/types.ts | 2 +- 4 files changed, 446 insertions(+), 11 deletions(-) create mode 100644 web/src/i18n/ko.ts diff --git a/web/src/components/LanguageSwitcher.tsx b/web/src/components/LanguageSwitcher.tsx index dc477021ee83f..cf07b350d65f0 100644 --- a/web/src/components/LanguageSwitcher.tsx +++ b/web/src/components/LanguageSwitcher.tsx @@ -3,32 +3,37 @@ import { Typography } from "@/components/NouiTypography"; import { useI18n } from "@/i18n/context"; /** - * Compact language toggle — shows a clickable flag that switches between - * English and Chinese. Persists choice to localStorage. + * Compact language toggle — cycles between English, Chinese, and Korean. + * Persists choice to localStorage. */ export function LanguageSwitcher() { const { locale, setLocale, t } = useI18n(); - const toggle = () => setLocale(locale === "en" ? "zh" : "en"); + const cycle = () => { + if (locale === "en") setLocale("zh"); + else if (locale === "zh") setLocale("ko"); + else setLocale("en"); + }; + + const flag = locale === "en" ? "🇬🇧" : locale === "zh" ? "🇨🇳" : "🇰🇷"; + const label = locale === "en" ? "EN" : locale === "zh" ? "中文" : "한국어"; return ( diff --git a/web/src/i18n/context.tsx b/web/src/i18n/context.tsx index 6fc6f6e56a05d..7c242a546539f 100644 --- a/web/src/i18n/context.tsx +++ b/web/src/i18n/context.tsx @@ -2,14 +2,15 @@ import { createContext, useContext, useState, useCallback, type ReactNode } from import type { Locale, Translations } from "./types"; import { en } from "./en"; import { zh } from "./zh"; +import { ko } from "./ko"; -const TRANSLATIONS: Record = { en, zh }; +const TRANSLATIONS: Record = { en, zh, ko }; const STORAGE_KEY = "hermes-locale"; function getInitialLocale(): Locale { try { const stored = localStorage.getItem(STORAGE_KEY); - if (stored === "en" || stored === "zh") return stored; + if (stored === "en" || stored === "zh" || stored === "ko") return stored; } catch { // SSR or privacy mode } diff --git a/web/src/i18n/ko.ts b/web/src/i18n/ko.ts new file mode 100644 index 0000000000000..f1696840ed286 --- /dev/null +++ b/web/src/i18n/ko.ts @@ -0,0 +1,429 @@ +import type { Translations } from "./types"; + +export const ko: Translations = { + common: { + save: "저장", + saving: "저장 중...", + cancel: "취소", + close: "닫기", + confirm: "확인", + delete: "삭제", + refresh: "새로고침", + retry: "다시 시도", + search: "검색...", + loading: "불러오는 중...", + create: "생성", + creating: "생성 중...", + set: "설정", + replace: "교체", + clear: "초기화", + live: "실시간", + off: "꺼짐", + enabled: "활성화됨", + disabled: "비활성화됨", + active: "활성", + inactive: "비활성", + unknown: "알 수 없음", + untitled: "제목 없음", + none: "없음", + form: "폼", + noResults: "결과 없음", + of: "/", + page: "페이지", + msgs: "메시지", + tools: "도구", + match: "일치", + other: "기타", + configured: "설정됨", + removed: "제거됨", + failedToToggle: "전환 실패", + failedToRemove: "제거 실패", + failedToReveal: "표시 실패", + collapse: "접기", + expand: "펼치기", + general: "일반", + messaging: "메시지", + pluginLoadFailed: + "플러그인 스크립트를 불러올 수 없습니다. Network 탭(dashboard-plugins/…)과 서버의 플러그인 경로를 확인하세요.", + pluginNotRegistered: + "플러그인 스크립트가 register()를 호출하지 않았거나 오류가 발생했습니다. 브라우저 콘솔을 확인하세요.", + }, + + app: { + brand: "Hermes Agent", + brandShort: "HA", + closeNavigation: "내비게이션 닫기", + closeModelTools: "모델 및 도구 닫기", + footer: { + org: "Nous Research", + }, + activeSessionsLabel: "활성 세션:", + gatewayStatusLabel: "게이트웨이 상태:", + gatewayStrip: { + failed: "시작 실패", + off: "꺼짐", + running: "실행 중", + starting: "시작 중", + stopped: "중지됨", + }, + nav: { + analytics: "분석", + chat: "채팅", + config: "설정", + cron: "스케줄", + documentation: "문서", + keys: "API 키", + logs: "로그", + models: "모델", + profiles: "프로필 : 멀티 에이전트", + plugins: "플러그인", + sessions: "세션", + skills: "스킬", + }, + modelToolsSheetSubtitle: "& 도구", + modelToolsSheetTitle: "모델", + navigation: "내비게이션", + openDocumentation: "새 탭에서 문서 열기", + openNavigation: "내비게이션 열기", + pluginNavSection: "플러그인", + sessionsActiveCount: "{count}개 활성", + statusOverview: "상태 개요", + system: "시스템", + webUi: "Web UI", + }, + + status: { + actionFailed: "액션 실패", + actionFinished: "완료", + actions: "액션", + agent: "에이전트", + activeSessions: "활성 세션", + connected: "연결됨", + connectedPlatforms: "연결된 플랫폼", + disconnected: "연결 끊김", + error: "오류", + failed: "실패", + gateway: "게이트웨이", + gatewayFailedToStart: "게이트웨이 시작 실패", + lastUpdate: "마지막 업데이트", + noneRunning: "없음", + notRunning: "실행 중 아님", + pid: "PID", + platformDisconnected: "연결 끊김", + platformError: "오류", + recentSessions: "최근 세션", + restartGateway: "게이트웨이 재시작", + restartingGateway: "게이트웨이 재시작 중…", + running: "실행 중", + runningRemote: "원격 실행 중", + startFailed: "시작 실패", + starting: "시작 중", + startedInBackground: "백그라운드에서 시작됨 — 진행 상황은 로그를 확인하세요", + stopped: "중지됨", + updateHermes: "Hermes 업데이트", + updatingHermes: "Hermes 업데이트 중…", + waitingForOutput: "출력 대기 중…", + }, + + sessions: { + title: "세션", + searchPlaceholder: "메시지 내용 검색...", + noSessions: "세션이 없습니다", + noMatch: "검색 결과가 없습니다", + startConversation: "대화를 시작하면 여기에 표시됩니다", + noMessages: "메시지 없음", + untitledSession: "제목 없는 세션", + deleteSession: "세션 삭제", + confirmDeleteTitle: "세션을 삭제하시겠습니까?", + confirmDeleteMessage: + "이 대화와 모든 메시지가 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다.", + sessionDeleted: "세션이 삭제되었습니다", + failedToDelete: "세션 삭제 실패", + resumeInChat: "채팅에서 이어보기", + previousPage: "이전 페이지", + nextPage: "다음 페이지", + roles: { + user: "사용자", + assistant: "어시스턴트", + system: "시스템", + tool: "도구", + }, + }, + + analytics: { + period: "기간:", + totalTokens: "총 토큰", + totalSessions: "총 세션", + apiCalls: "API 호출", + dailyTokenUsage: "일별 토큰 사용량", + dailyBreakdown: "일별 세부 내역", + perModelBreakdown: "모델별 세부 내역", + topSkills: "상위 스킬", + skill: "스킬", + loads: "에이전트 로드", + edits: "에이전트 관리", + lastUsed: "마지막 사용", + input: "입력", + output: "출력", + total: "합계", + noUsageData: "해당 기간의 사용 데이터가 없습니다", + startSession: "세션을 시작하면 분석 데이터가 표시됩니다", + date: "날짜", + model: "모델", + tokens: "토큰", + perDayAvg: "/일 평균", + acrossModels: "{count}개 모델 사용", + inOut: "입력 {input} / 출력 {output}", + }, + + models: { + modelsUsed: "사용한 모델", + estimatedCost: "예상 비용", + tokens: "토큰", + sessions: "세션", + avgPerSession: "세션 평균", + apiCalls: "API 호출", + toolCalls: "도구 호출", + noModelsData: "해당 기간의 모델 사용 데이터가 없습니다", + startSession: "세션을 시작하면 모델 데이터가 표시됩니다", + }, + + logs: { + title: "로그", + autoRefresh: "자동 새로고침", + file: "파일", + level: "레벨", + component: "컴포넌트", + lines: "줄", + noLogLines: "로그가 없습니다", + }, + + cron: { + confirmDeleteMessage: + "이 작업을 스케줄에서 제거합니다. 이 작업은 되돌릴 수 없습니다.", + confirmDeleteTitle: "스케줄 작업을 삭제하시겠습니까?", + newJob: "새 크론 작업", + nameOptional: "이름 (선택)", + namePlaceholder: "예: 일일 요약", + prompt: "프롬프트", + promptPlaceholder: "각 실행 시 에이전트가 할 작업을 입력하세요", + schedule: "스케줄 (크론 표현식)", + schedulePlaceholder: "0 9 * * *", + deliverTo: "전달 대상", + scheduledJobs: "스케줄된 작업", + noJobs: "크론 작업이 없습니다. 위에서 생성하세요.", + last: "마지막", + next: "다음", + pause: "일시정지", + resume: "재개", + triggerNow: "즉시 실행", + delivery: { + local: "로컬", + telegram: "텔레그램", + discord: "디스코드", + slack: "슬랙", + email: "이메일", + }, + }, + + profiles: { + newProfile: "새 프로필", + name: "이름", + namePlaceholder: "예: coder, writer 등", + nameRequired: "이름은 필수입니다", + nameRule: + "소문자, 숫자, _ 및 -만 사용 가능합니다. 문자 또는 숫자로 시작해야 하며 최대 64자입니다.", + invalidName: "잘못된 프로필 이름", + cloneFromDefault: "기본 프로필에서 설정 복제", + allProfiles: "프로필", + noProfiles: "프로필이 없습니다.", + defaultBadge: "기본", + hasEnv: "env", + model: "모델", + skills: "스킬", + rename: "이름 변경", + editSoul: "SOUL.md 편집", + soulSection: "SOUL.md (페르소나 / 시스템 프롬프트)", + soulPlaceholder: "# 이 에이전트의 동작 방식…", + saveSoul: "SOUL 저장", + soulSaved: "SOUL.md 저장됨", + openInTerminal: "CLI 명령 복사", + commandCopied: "클립보드에 복사됨", + copyFailed: "복사 실패", + confirmDeleteTitle: "프로필을 삭제하시겠습니까?", + confirmDeleteMessage: + "프로필 '{name}'의 설정, 키, 메모리, 세션, 스킬, 크론 작업이 영구적으로 삭제됩니다. 되돌릴 수 없습니다.", + created: "생성됨", + deleted: "삭제됨", + renamed: "이름 변경됨", + }, + + pluginsPage: { + contextEngineLabel: "컨텍스트 엔진", + dashboardSlots: "대시보드 슬롯", + disableRuntime: "비활성화", + enableAfterInstall: "설치 후 활성화", + enableRuntime: "활성화", + forceReinstall: "강제 재설치 (기존 폴더 삭제 후)", + headline: + "Hermes 플러그인을 검색, 설치, 활성화 및 업데이트합니다 (`hermes plugins` 동일).", + identifierLabel: "Git URL 또는 owner/repo", + inactive: "비활성", + installBtn: "Git에서 설치", + installHeading: "GitHub / Git URL에서 설치", + installHint: "owner/repo 단축형 또는 https:// 또는 git@ 클론 URL을 사용하세요.", + memoryProviderLabel: "메모리 프로바이더", + missingEnvWarn: "플러그인 실행 전 Keys에서 설정하세요:", + noDashboardTab: "대시보드 탭 없음", + openTab: "열기", + orphanHeading: "대시보드 전용 확장 (에이전트 plugin.yaml 없음)", + pluginListHeading: "설치된 플러그인", + providerDefaults: "기본 내장", + providersHeading: "런타임 프로바이더 플러그인", + providersHint: + "memory.provider (비어있으면 내장)와 context.engine을 config.yaml에 씁니다. 다음 세션부터 적용됩니다.", + refreshDashboard: "대시보드 확장 재스캔", + removeConfirm: "~/.hermes/plugins/에서 이 플러그인을 제거하시겠습니까?", + removeHint: "~/.hermes/plugins/ 아래 사용자 설치 플러그인만 제거할 수 있습니다.", + rescanHeading: "SPA 플러그인 레지스트리", + rescanHint: "디스크에 파일 추가 후 재스캔하면 사이드바에 새 매니페스트가 표시됩니다.", + runtimeHeading: "게이트웨이 런타임 (YAML 플러그인)", + saveProviders: "프로바이더 설정 저장", + savedProviders: "프로바이더 설정이 저장되었습니다.", + sourceBadge: "소스", + authRequired: "인증 필요", + authRequiredHint: "인증하려면 다음 명령을 실행하세요:", + updateGit: "Git pull", + versionBadge: "버전", + showInSidebar: "사이드바에 표시", + hideFromSidebar: "사이드바에서 숨기기", + }, + + skills: { + title: "스킬", + searchPlaceholder: "스킬 및 툴셋 검색...", + enabledOf: "{enabled}/{total} 활성화", + all: "전체", + categories: "카테고리", + filters: "필터", + noSkills: "스킬이 없습니다. 스킬은 ~/.hermes/skills/에서 불러옵니다", + noSkillsMatch: "검색 또는 필터에 맞는 스킬이 없습니다.", + skillCount: "{count}개 스킬", + resultCount: "{count}개 결과", + noDescription: "설명이 없습니다.", + toolsets: "툴셋", + toolsetLabel: "{name} 툴셋", + noToolsetsMatch: "검색에 맞는 툴셋이 없습니다.", + setupNeeded: "설정 필요", + disabledForCli: "CLI에서 비활성화됨", + more: "+{count}개 더", + }, + + config: { + configPath: "~/.hermes/config.yaml", + filters: "필터", + sections: "섹션", + exportConfig: "설정을 JSON으로 내보내기", + importConfig: "JSON에서 설정 가져오기", + resetDefaults: "기본값으로 초기화", + resetScopeTooltip: "{scope} 기본값으로 초기화", + confirmResetScope: "{scope} 설정을 기본값으로 초기화하시겠습니까? 폼만 업데이트되며 저장을 눌러야 config.yaml에 반영됩니다.", + resetScopeToast: "{scope} 기본값으로 초기화됨 — 확인 후 저장하세요", + rawYaml: "YAML 설정 원문", + searchResults: "검색 결과", + fields: "필드", + noFieldsMatch: '"{query}"에 맞는 필드가 없습니다', + configSaved: "설정이 저장되었습니다", + yamlConfigSaved: "YAML 설정이 저장되었습니다", + failedToSave: "저장 실패", + failedToSaveYaml: "YAML 저장 실패", + failedToLoadRaw: "설정 원문 불러오기 실패", + configImported: "설정 가져옴 — 확인 후 저장하세요", + invalidJson: "잘못된 JSON 파일", + categories: { + general: "일반", + agent: "에이전트", + terminal: "터미널", + display: "화면", + delegation: "위임", + memory: "메모리", + compression: "압축", + security: "보안", + browser: "브라우저", + voice: "음성", + tts: "텍스트 음성 변환", + stt: "음성 텍스트 변환", + logging: "로깅", + discord: "디스코드", + auxiliary: "보조", + }, + }, + + env: { + changesNote: "변경사항은 즉시 저장됩니다. 활성 세션은 새 키를 자동으로 반영합니다.", + confirmClearMessage: + "이 변수의 저장된 값이 .env 파일에서 제거됩니다. UI에서는 되돌릴 수 없습니다.", + confirmClearTitle: "이 키를 초기화하시겠습니까?", + description: "API 키 및 시크릿 관리:", + hideAdvanced: "고급 숨기기", + showAdvanced: "고급 표시", + llmProviders: "LLM 프로바이더", + providersConfigured: "{total}개 중 {configured}개 설정됨", + getKey: "키 발급", + notConfigured: "{count}개 미설정", + notSet: "설정되지 않음", + keysCount: "{count}개 키", + enterValue: "값 입력...", + replaceCurrentValue: "현재 값 교체 ({preview})", + showValue: "실제 값 표시", + hideValue: "값 숨기기", + }, + + oauth: { + title: "프로바이더 로그인 (OAuth)", + providerLogins: "프로바이더 로그인 (OAuth)", + description: "{total}개 중 {connected}개 OAuth 프로바이더 연결됨. 로그인은 CLI를 통해 진행됩니다. 명령 복사 후 터미널에 붙여넣으세요.", + connected: "연결됨", + expired: "만료됨", + notConnected: "연결되지 않음. 터미널에서 {command}를 실행하세요.", + runInTerminal: "터미널에서 실행", + noProviders: "OAuth 지원 프로바이더가 없습니다.", + login: "로그인", + disconnect: "연결 끊기", + managedExternally: "외부에서 관리됨", + copied: "복사됨 ✓", + cli: "CLI", + copyCliCommand: "CLI 명령 복사 (외부/대체용)", + connect: "연결", + sessionExpires: "{time} 후 만료", + initiatingLogin: "로그인 진행 중…", + exchangingCode: "토큰으로 코드 교환 중…", + connectedClosing: "연결됨! 닫는 중…", + loginFailed: "로그인 실패.", + sessionExpired: "세션이 만료되었습니다. 다시 시도를 클릭하세요.", + reOpenAuth: "인증 페이지 다시 열기", + reOpenVerification: "인증 확인 페이지 다시 열기", + submitCode: "코드 제출", + pasteCode: "인증 코드 붙여넣기 (#state 접미사 포함 가능)", + waitingAuth: "브라우저에서 인증 대기 중…", + enterCodePrompt: "새 탭이 열렸습니다. 요청 시 이 코드를 입력하세요:", + pkceStep1: "claude.ai로 새 탭이 열렸습니다. 로그인 후 승인을 클릭하세요.", + pkceStep2: "승인 후 표시된 인증 코드를 복사하세요.", + pkceStep3: "아래에 붙여넣고 제출하세요.", + flowLabels: { + pkce: "브라우저 로그인 (PKCE)", + device_code: "디바이스 코드", + external: "외부 CLI", + }, + expiresIn: "{time} 후 만료", + }, + + language: { + switchTo: "한국어로 전환", + }, + + theme: { + title: "테마", + switchTheme: "테마 변경", + }, +}; diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts index d93260d26d704..a595b11fe18d4 100644 --- a/web/src/i18n/types.ts +++ b/web/src/i18n/types.ts @@ -1,4 +1,4 @@ -export type Locale = "en" | "zh"; +export type Locale = "en" | "zh" | "ko"; export interface Translations { // ── Common ──