From 2076b8cbb6b5ab59663aeb3f5da4964e25503c27 Mon Sep 17 00:00:00 2001 From: moduvoice Date: Fri, 10 Jul 2026 11:27:25 +0700 Subject: [PATCH] feat(dashboard): add Korean (ko) translation for all i18n namespaces Adds a ko.ts sibling to every locale namespace (common + all 9 features) with full key parity to en, registers ko in SUPPORTED_LANGUAGES/ LANGUAGE_NAMES and the i18n resource aggregation, and extends the locale-parity test to also check en/ko parity for the namespaces it already covers. --- dashboard/src/common/locales/ko.ts | 94 ++ dashboard/src/features/api-keys/locales/ko.ts | 145 +++ dashboard/src/features/auth/locales/ko.ts | 79 ++ .../src/features/databases/locales/ko.ts | 315 ++++++ dashboard/src/features/keyvalue/locales/ko.ts | 326 ++++++ dashboard/src/features/logs/locales/ko.ts | 69 ++ dashboard/src/features/metrics/locales/ko.ts | 126 +++ dashboard/src/features/services/locales/ko.ts | 955 ++++++++++++++++++ dashboard/src/features/team/locales/ko.ts | 155 +++ dashboard/src/features/usage/locales/ko.ts | 70 ++ .../src/features/workspaces/locales/ko.ts | 185 ++++ .../src/i18n/__tests__/locale-parity.test.ts | 27 +- dashboard/src/i18n/config.ts | 3 +- dashboard/src/i18n/index.ts | 26 + 14 files changed, 2568 insertions(+), 7 deletions(-) create mode 100644 dashboard/src/common/locales/ko.ts create mode 100644 dashboard/src/features/api-keys/locales/ko.ts create mode 100644 dashboard/src/features/auth/locales/ko.ts create mode 100644 dashboard/src/features/databases/locales/ko.ts create mode 100644 dashboard/src/features/keyvalue/locales/ko.ts create mode 100644 dashboard/src/features/logs/locales/ko.ts create mode 100644 dashboard/src/features/metrics/locales/ko.ts create mode 100644 dashboard/src/features/services/locales/ko.ts create mode 100644 dashboard/src/features/team/locales/ko.ts create mode 100644 dashboard/src/features/usage/locales/ko.ts create mode 100644 dashboard/src/features/workspaces/locales/ko.ts diff --git a/dashboard/src/common/locales/ko.ts b/dashboard/src/common/locales/ko.ts new file mode 100644 index 00000000..29805589 --- /dev/null +++ b/dashboard/src/common/locales/ko.ts @@ -0,0 +1,94 @@ +import type { TranslationEntry } from "@/i18n"; + +const koCommon: Record = { + "common.appName": { + message: "bex", + description: "Product name shown in the dashboard chrome", + }, + "common.loading": { + message: "로딩 중…", + description: "Generic loading state label", + }, + "common.navDashboardGroup": { + message: "대시보드", + description: "Sidebar nav section label", + }, + "common.navServices": { + message: "서비스", + description: "Sidebar nav link to the services list page", + }, + "common.navDatabases": { + message: "데이터베이스", + description: "Sidebar nav link to the databases list page", + }, + "common.navKeyValue": { + message: "키-값 저장소", + description: "Sidebar nav link to the Key Value list page", + }, + "common.navUsage": { + message: "사용량", + description: "Sidebar nav link to the workspace usage page", + }, + "common.navSettings": { + message: "설정", + description: "Sidebar nav link to the account settings page", + }, + "common.changeLanguage": { + message: "언어 변경", + description: "Accessible label for the language switcher button", + }, + "common.userMenuSettings": { + message: "설정", + description: "User menu item that navigates to account settings", + }, + "common.userMenuTheme": { + message: "테마", + description: "User menu submenu label for theme selection", + }, + "common.userMenuThemeLight": { + message: "라이트", + description: "Light theme option label", + }, + "common.userMenuThemeDark": { + message: "다크", + description: "Dark theme option label", + }, + "common.userMenuThemeSystem": { + message: "시스템", + description: "System theme option label", + }, + "common.userMenuLogOut": { + message: "로그아웃", + description: "User menu item that signs the user out", + }, + "common.goHome": { + message: "홈으로", + description: "Button label that navigates back to the home page", + }, + "common.goBack": { + message: "뒤로 가기", + description: "Button label that navigates to the previous page", + }, + "common.tryAgain": { + message: "다시 시도", + description: "Button label that retries after an error", + }, + "common.notFoundTitle": { + message: "페이지를 찾을 수 없습니다", + description: "404 page heading", + }, + "common.notFoundDescription": { + message: "찾으시는 페이지가 존재하지 않거나 이동되었습니다.", + description: "404 page explanatory text", + }, + "common.errorTitle": { + message: "문제가 발생했습니다", + description: "Generic error page heading", + }, + "common.errorDefaultMessage": { + message: "문제가 발생했습니다.", + description: "Fallback error message when none is provided", + }, +}; + +export default koCommon; diff --git a/dashboard/src/features/api-keys/locales/ko.ts b/dashboard/src/features/api-keys/locales/ko.ts new file mode 100644 index 00000000..370fe9aa --- /dev/null +++ b/dashboard/src/features/api-keys/locales/ko.ts @@ -0,0 +1,145 @@ +import type { TranslationEntry } from "@/i18n"; + +const koApiKeys: Record = { + "apiKeys.title": { + message: "API 키", + description: "Settings API Keys section card title", + }, + "apiKeys.description": { + message: + "스크립트와 에이전트를 위한 머신 자격 증명입니다. 워크스페이스 전체에서 공유되며, 키를 관리할 수 있는 사람은 자신의 키뿐 아니라 모든 키를 볼 수 있습니다.", + description: "Settings API Keys section card description", + }, + "apiKeys.colName": { + message: "이름", + description: "API Keys table column header", + }, + "apiKeys.colCreated": { + message: "생성일", + description: "API Keys table column header", + }, + "apiKeys.colCreatedBy": { + message: "생성자", + description: "API Keys table column header — who minted the key", + }, + "apiKeys.colLastUsed": { + message: "마지막 사용", + description: + "API Keys table column header — when a token for the key was last used", + }, + "apiKeys.neverUsed": { + message: "없음", + description: "API Keys last-used cell when the key has never been used", + }, + "apiKeys.emptyTitle": { + message: "API 키가 없습니다", + description: "API Keys empty-state title", + }, + "apiKeys.emptyBody": { + message: "스크립트나 에이전트를 인증할 키를 생성하세요.", + description: "API Keys empty-state body", + }, + "apiKeys.forbiddenTitle": { + message: "권한 없음", + description: "API Keys state when the caller lacks permission (403)", + }, + "apiKeys.forbiddenBody": { + message: "이 워크스페이스의 API 키를 관리할 권한이 없습니다.", + description: "API Keys forbidden-state body", + }, + "apiKeys.errorTitle": { + message: "API 키를 불러오지 못했습니다", + description: "API Keys generic error title", + }, + "apiKeys.errorBody": { + message: "문제가 발생했습니다. 다시 시도해 주세요.", + description: "API Keys generic error body", + }, + "apiKeys.create": { + message: "API 키 생성", + description: "Button that opens the mint dialog", + }, + "apiKeys.createTitle": { + message: "API 키 생성", + description: "Mint dialog title (name step)", + }, + "apiKeys.createDescription": { + message: "나중에 알아볼 수 있도록 이 키의 이름을 지정하세요.", + description: "Mint dialog description (name step)", + }, + "apiKeys.fieldName": { + message: "이름", + description: "Mint dialog name field label", + }, + "apiKeys.fieldNamePlaceholder": { + message: "예: deploy-agent", + description: "Mint dialog name field placeholder", + }, + "apiKeys.createCancel": { + message: "취소", + description: "Mint dialog cancel button (name step)", + }, + "apiKeys.createSubmit": { + message: "생성", + description: "Mint dialog submit button (name step)", + }, + "apiKeys.createdTitle": { + message: "API 키가 생성되었습니다", + description: "Mint dialog title (secret-shown step)", + }, + "apiKeys.createdWarning": { + message: "지금 이 키를 복사하세요 — 다시 볼 수 없습니다.", + description: "Mint dialog warning (secret-shown step)", + }, + "apiKeys.createdDone": { + message: "완료", + description: "Mint dialog close button (secret-shown step)", + }, + "apiKeys.copy": { + message: "복사", + description: "Copy-to-clipboard icon button label", + }, + "apiKeys.copied": { + message: "클립보드에 복사됨", + description: "Toast on a successful secret copy", + }, + "apiKeys.copyError": { + message: "클립보드에 복사하지 못했습니다", + description: "Toast on a failed secret copy", + }, + "apiKeys.createSuccess": { + message: "{name} 생성됨", + description: "Toast on a successful mint", + }, + "apiKeys.createError": { + message: "{name} 생성하지 못했습니다", + description: "Toast on a failed mint", + }, + "apiKeys.revoke": { + message: "폐기", + description: "Row action / confirmation button to revoke a key", + }, + "apiKeys.revokeConfirmTitle": { + message: "{name}을(를) 폐기하시겠습니까?", + description: "Revoke-confirmation dialog title", + }, + "apiKeys.revokeConfirmBody": { + message: + "이 키로 인증하는 모든 것이 즉시 작동을 멈춥니다. 이 작업은 되돌릴 수 없습니다.", + description: "Revoke-confirmation dialog body", + }, + "apiKeys.revokeCancel": { + message: "취소", + description: "Revoke-confirmation dialog cancel button", + }, + "apiKeys.revokeSuccess": { + message: "{name} 폐기됨", + description: "Toast on a successful revoke", + }, + "apiKeys.revokeError": { + message: "{name} 폐기하지 못했습니다", + description: "Toast on a failed revoke", + }, +}; + +export default koApiKeys; diff --git a/dashboard/src/features/auth/locales/ko.ts b/dashboard/src/features/auth/locales/ko.ts new file mode 100644 index 00000000..6adfb41d --- /dev/null +++ b/dashboard/src/features/auth/locales/ko.ts @@ -0,0 +1,79 @@ +import type { TranslationEntry } from "@/i18n"; + +const koAuth: Record = { + "auth.loginTitle": { + message: "다시 오신 것을 환영합니다", + description: "Login page hero title", + }, + "auth.loginSubtitle": { + message: "계정에 로그인하세요", + description: "Login page hero subtitle", + }, + "auth.registerTitle": { + message: "계정 만들기", + description: "Registration page hero title", + }, + "auth.registerSubtitle": { + message: "시작하려면 정보를 입력하세요", + description: "Registration page hero subtitle", + }, + "auth.forgotPasswordTitle": { + message: "비밀번호 재설정", + description: "Forgot-password page hero title", + }, + "auth.forgotPasswordSubtitle": { + message: "복구 코드를 받으려면 이메일을 입력하세요", + description: "Forgot-password page hero subtitle", + }, + "auth.settingsTitle": { + message: "설정", + description: "Account settings page heading", + }, + "auth.settingsSubtitle": { + message: "계정 프로필과 비밀번호를 관리하세요.", + description: "Account settings page subheading", + }, + "auth.loggingOutTitle": { + message: "로그아웃 중...", + description: "Logout page heading while the logout request is in flight", + }, + "auth.loggingOutSubtitle": { + message: "세션을 종료하는 중입니다. 잠시만 기다려 주세요.", + description: "Logout page subtext while the logout request is in flight", + }, + "auth.loggedOutTitle": { + message: "로그아웃되었습니다", + description: "Logout page heading once logout has completed", + }, + "auth.loggedOutSubtitle": { + message: "로그인 페이지로 이동합니다…", + description: "Logout page subtext once logout has completed", + }, + "auth.featureSecureTitle": { + message: "기본적으로 안전합니다", + description: "Auth hero feature bullet title", + }, + "auth.featureSecureDescription": { + message: + "세션은 Ory Kratos가 관리합니다 — 직접 만든 인증 시스템이 아닌, 검증된 아이덴티티 인프라입니다.", + description: "Auth hero feature bullet description", + }, + "auth.featureDashboardTitle": { + message: "모든 서비스를 위한 하나의 대시보드", + description: "Auth hero feature bullet title", + }, + "auth.featureDashboardDescription": { + message: "bex에서 실행되는 모든 것을 한 곳에서 배포, 모니터링, 관리하세요.", + description: "Auth hero feature bullet description", + }, + "auth.featureOpenSourceTitle": { + message: "오픈소스로 만들어졌습니다", + description: "Auth hero feature bullet title", + }, + "auth.featureOpenSourceDescription": { + message: "bex는 오픈소스 Render 대안입니다.", + description: "Auth hero feature bullet description", + }, +}; + +export default koAuth; diff --git a/dashboard/src/features/databases/locales/ko.ts b/dashboard/src/features/databases/locales/ko.ts new file mode 100644 index 00000000..fdee495d --- /dev/null +++ b/dashboard/src/features/databases/locales/ko.ts @@ -0,0 +1,315 @@ +import type { TranslationEntry } from "@/i18n"; + +const koDatabases: Record = { + // --- List page stat tiles --- + "databases.statTotal": { + message: "전체 데이터베이스", + description: "Databases page stat card label", + }, + "databases.statAvailable": { + message: "사용 가능", + description: "Databases page stat card label (healthy databases)", + }, + "databases.statCreating": { + message: "생성 중", + description: "Databases page stat card label (provisioning databases)", + }, + // --- List table --- + "databases.cardTitle": { + message: "데이터베이스", + description: "Databases table card title", + }, + "databases.colName": { + message: "이름", + description: "Databases table column header", + }, + "databases.colStatus": { + message: "상태", + description: "Databases table column header", + }, + "databases.colPlan": { + message: "플랜", + description: "Databases table column header (instance type / tier)", + }, + "databases.colVersion": { + message: "버전", + description: "Databases table column header (PostgreSQL major version)", + }, + "databases.colStorage": { + message: "스토리지", + description: "Databases table column header (disk size)", + }, + "databases.colCreated": { + message: "생성일", + description: "Databases table column header (relative age from createdAt)", + }, + "databases.colActions": { + message: "작업", + description: "Databases table actions column header (screen-reader only)", + }, + // --- Status badges --- + "databases.statusAvailable": { + message: "사용 가능", + description: "Database status badge (CNPG cluster healthy)", + }, + "databases.statusCreating": { + message: "생성 중", + description: "Database status badge (provisioning)", + }, + "databases.statusUnavailable": { + message: "사용 불가", + description: "Database status badge (provisioning failed)", + }, + "databases.statusUnknown": { + message: "알 수 없음", + description: "Database status badge for an unrecognized status", + }, + // --- List states --- + "databases.errorTitle": { + message: "데이터베이스를 불러오지 못했습니다", + description: "Databases list error card title", + }, + "databases.errorBody": { + message: "bex-api 요청이 실패했습니다. 연결을 확인하고 다시 시도하세요.", + description: "Databases list error card body", + }, + "databases.emptyTitle": { + message: "아직 데이터베이스가 없습니다", + description: "Databases list empty state title", + }, + "databases.emptyBody": { + message: "첫 번째 관리형 Postgres를 만들면 여기에 표시됩니다.", + description: "Databases list empty state body", + }, + // --- Row actions / delete --- + "databases.actionsMenu": { + message: "작업 메뉴 열기", + description: "Accessible label for the per-row actions trigger", + }, + "databases.actionDelete": { + message: "삭제", + description: "Row action: permanently delete the database", + }, + "databases.deleteConfirmTitle": { + message: "{name}을(를) 삭제하시겠습니까?", + description: "Delete-confirmation dialog title", + }, + "databases.deleteConfirmBody": { + message: + "이 작업은 데이터베이스와 모든 데이터 — Postgres 클러스터, 스토리지, 연결 자격 증명 —를 영구적으로 삭제합니다. 되돌릴 수 없습니다.", + description: "Delete-confirmation dialog body", + }, + "databases.deleteConfirmPrompt": { + message: "확인하려면 {name}을(를) 입력하세요.", + description: "Delete-confirmation typed-name prompt label", + }, + "databases.deleteCancel": { + message: "취소", + description: "Delete-confirmation dialog cancel button", + }, + "databases.deleteConfirm": { + message: "데이터베이스 삭제", + description: "Delete-confirmation dialog confirm button", + }, + "databases.deleteSuccess": { + message: "{name} 삭제 중…", + description: "Toast after a delete request is accepted", + }, + "databases.deleteError": { + message: "{name} 삭제하지 못했습니다. 다시 시도해 주세요.", + description: "Toast when a delete request fails", + }, + // --- Create dialog --- + "databases.createButton": { + message: "새 데이터베이스", + description: "Button that opens the create-database dialog", + }, + "databases.createTitle": { + message: "Postgres 데이터베이스 생성", + description: "Create-database dialog title", + }, + "databases.createDescription": { + message: "관리형 PostgreSQL 인스턴스를 프로비저닝합니다.", + description: "Create-database dialog description", + }, + "databases.fieldName": { + message: "이름", + description: "Create-database form field label (database name)", + }, + "databases.fieldNamePlaceholder": { + message: "my-database", + description: "Create-database name input placeholder", + }, + "databases.fieldNameError": { + message: "소문자, 숫자, 하이픈만 사용하세요. 문자로 시작해야 합니다.", + description: "Create-database name validation message", + }, + "databases.fieldPlan": { + message: "인스턴스 유형", + description: "Create-database form field label (plan / tier)", + }, + "databases.fieldPlanPlaceholder": { + message: "인스턴스 유형 선택", + description: "Create-database plan select placeholder", + }, + "databases.fieldVersion": { + message: "PostgreSQL 버전", + description: "Create-database form field label (major version)", + }, + "databases.fieldVersionDefault": { + message: "기본값(최신)", + description: "Create-database version select default option", + }, + "databases.fieldDisk": { + message: "디스크 크기 (GB)", + description: "Create-database form field label (storage size)", + }, + "databases.fieldPublic": { + message: "공개 액세스", + description: "Create-database form field label (external endpoint toggle)", + }, + "databases.fieldPublicHint": { + message: "TLS를 통해 클러스터 외부에서의 연결을 허용합니다.", + description: "Create-database public toggle helper text", + }, + "databases.createCancel": { + message: "취소", + description: "Create-database dialog cancel button", + }, + "databases.createSubmit": { + message: "데이터베이스 생성", + description: "Create-database dialog submit button", + }, + "databases.createSuccess": { + message: "{name} 생성 중…", + description: + "Toast after a create request is accepted (provisioning is async)", + }, + "databases.createError": { + message: "{name} 생성하지 못했습니다. 다시 시도해 주세요.", + description: "Toast when a create request fails", + }, + // --- Detail metadata --- + "databases.metaTitle": { + message: "상세 정보", + description: "Database detail metadata card title", + }, + "databases.metaStatus": { + message: "상태", + description: "Database detail metadata row label", + }, + "databases.metaPlan": { + message: "인스턴스 유형", + description: "Database detail metadata row label", + }, + "databases.metaVersion": { + message: "버전", + description: "Database detail metadata row label", + }, + "databases.metaDatabaseName": { + message: "데이터베이스", + description: "Database detail metadata row label (the normalized db name)", + }, + "databases.metaDatabaseUser": { + message: "사용자", + description: "Database detail metadata row label (owner role)", + }, + "databases.metaStorage": { + message: "스토리지", + description: "Database detail metadata row label (disk size)", + }, + "databases.metaHighAvailability": { + message: "고가용성", + description: + "Database detail metadata row label (HA — single-instance MVP is No)", + }, + "databases.metaPublic": { + message: "공개 액세스", + description: "Database detail metadata row label (external endpoint)", + }, + "databases.metaExternalHost": { + message: "외부 호스트", + description: "Database detail metadata row label (SNI hostname)", + }, + "databases.metaCreated": { + message: "생성일", + description: "Database detail metadata row label (relative age)", + }, + "databases.yes": { + message: "예", + description: "Metadata value for a true boolean field", + }, + "databases.no": { + message: "아니요", + description: "Metadata value for a false boolean field", + }, + "databases.notFoundTitle": { + message: "데이터베이스를 찾을 수 없습니다", + description: "Detail page state when database(id) returns nothing", + }, + "databases.notFoundBody": { + message: "{name} 이름의 데이터베이스가 없거나 접근 권한이 없습니다.", + description: "Detail page not-found body", + }, + // --- Connection info panel --- + "databases.connTitle": { + message: "연결", + description: "Connection-info panel card title", + }, + "databases.connDescription": { + message: + "연결 문자열과 데이터베이스 비밀번호입니다. 요청할 때만 표시되며 자동으로 노출되지 않습니다.", + description: "Connection-info panel card description", + }, + "databases.connReveal": { + message: "연결 정보 표시", + description: "Button that fetches the connection info on demand", + }, + "databases.connHide": { + message: "연결 정보 숨기기", + description: "Button that clears the revealed connection info", + }, + "databases.connPassword": { + message: "비밀번호", + description: "Connection-info field label (database password)", + }, + "databases.connShowPassword": { + message: "비밀번호 표시", + description: "Accessible label to unmask the password", + }, + "databases.connHidePassword": { + message: "비밀번호 숨기기", + description: "Accessible label to re-mask the password", + }, + "databases.connInternal": { + message: "내부 연결 문자열", + description: "Connection-info field label (in-cluster URL)", + }, + "databases.connExternal": { + message: "외부 연결 문자열", + description: "Connection-info field label (public URL, TLS)", + }, + "databases.connPsql": { + message: "psql 명령어", + description: "Connection-info field label (ready-to-run psql command)", + }, + "databases.connErrorTitle": { + message: "연결 정보를 불러오지 못했습니다", + description: "Connection-info panel error title", + }, + "databases.connErrorBody": { + message: + "데이터베이스가 아직 프로비저닝 중이거나 자격 증명을 볼 권한이 없을 수 있습니다.", + description: "Connection-info panel error body", + }, + "databases.copied": { + message: "클립보드에 복사됨", + description: "Toast after copying a connection field", + }, + "databases.copyError": { + message: "클립보드에 복사하지 못했습니다", + description: "Toast when clipboard copy fails", + }, +}; + +export default koDatabases; diff --git a/dashboard/src/features/keyvalue/locales/ko.ts b/dashboard/src/features/keyvalue/locales/ko.ts new file mode 100644 index 00000000..a0c5de07 --- /dev/null +++ b/dashboard/src/features/keyvalue/locales/ko.ts @@ -0,0 +1,326 @@ +import type { TranslationEntry } from "@/i18n"; + +const koKeyValue: Record = { + // --- List page stat tiles --- + "keyvalue.statTotal": { + message: "전체 키-값 저장소", + description: "Key Value page stat card label", + }, + "keyvalue.statAvailable": { + message: "사용 가능", + description: "Key Value page stat card label (healthy stores)", + }, + "keyvalue.statCreating": { + message: "생성 중", + description: "Key Value page stat card label (provisioning stores)", + }, + // --- List table --- + "keyvalue.cardTitle": { + message: "키-값 저장소", + description: "Key Value table card title", + }, + "keyvalue.colName": { + message: "이름", + description: "Key Value table column header", + }, + "keyvalue.colStatus": { + message: "상태", + description: "Key Value table column header", + }, + "keyvalue.colPlan": { + message: "인스턴스 유형", + description: "Key Value table column header (plan / tier)", + }, + "keyvalue.colVersion": { + message: "버전", + description: "Key Value table column header (Valkey version)", + }, + "keyvalue.colCreated": { + message: "생성일", + description: "Key Value table column header (relative age from createdAt)", + }, + "keyvalue.colActions": { + message: "작업", + description: "Key Value table actions column header (screen-reader only)", + }, + // --- Status badges --- + "keyvalue.statusAvailable": { + message: "사용 가능", + description: "Key Value status badge (Valkey instance healthy)", + }, + "keyvalue.statusCreating": { + message: "생성 중", + description: "Key Value status badge (provisioning)", + }, + "keyvalue.statusUnavailable": { + message: "사용 불가", + description: "Key Value status badge (provisioning failed)", + }, + "keyvalue.statusUnknown": { + message: "알 수 없음", + description: "Key Value status badge for an unrecognized status", + }, + // --- List states --- + "keyvalue.errorTitle": { + message: "키-값 저장소를 불러오지 못했습니다", + description: "Key Value list error card title", + }, + "keyvalue.errorBody": { + message: "bex-api 요청이 실패했습니다. 연결을 확인하고 다시 시도하세요.", + description: "Key Value list error card body", + }, + "keyvalue.emptyTitle": { + message: "아직 키-값 저장소가 없습니다", + description: "Key Value list empty state title", + }, + "keyvalue.emptyBody": { + message: "첫 번째 관리형 키-값 저장소를 만들면 여기에 표시됩니다.", + description: "Key Value list empty state body", + }, + // --- Row actions / delete --- + "keyvalue.actionsMenu": { + message: "작업 메뉴 열기", + description: "Accessible label for the per-row actions trigger", + }, + "keyvalue.actionDelete": { + message: "삭제", + description: "Row action: permanently delete the Key Value store", + }, + "keyvalue.deleteConfirmTitle": { + message: "{name}을(를) 삭제하시겠습니까?", + description: "Delete-confirmation dialog title", + }, + "keyvalue.deleteConfirmBody": { + message: + "이 작업은 키-값 저장소와 모든 데이터 — Valkey 인스턴스, 스토리지, 연결 자격 증명 —를 영구적으로 삭제합니다. 되돌릴 수 없습니다.", + description: "Delete-confirmation dialog body", + }, + "keyvalue.deleteConfirmPrompt": { + message: "확인하려면 {name}을(를) 입력하세요.", + description: "Delete-confirmation typed-name prompt label", + }, + "keyvalue.deleteCancel": { + message: "취소", + description: "Delete-confirmation dialog cancel button", + }, + "keyvalue.deleteConfirm": { + message: "키-값 저장소 삭제", + description: "Delete-confirmation dialog confirm button", + }, + "keyvalue.deleteSuccess": { + message: "{name} 삭제 중…", + description: "Toast after a delete request is accepted", + }, + "keyvalue.deleteError": { + message: "{name} 삭제하지 못했습니다. 다시 시도해 주세요.", + description: "Toast when a delete request fails", + }, + // --- Create form --- + "keyvalue.createButton": { + message: "새 키-값 저장소", + description: "Button that navigates to the create-Key-Value page", + }, + "keyvalue.createTitle": { + message: "키-값 저장소 생성", + description: "Create-Key-Value page title", + }, + "keyvalue.createDescription": { + message: "관리형 Valkey(Redis 호환) 인스턴스를 프로비저닝합니다.", + description: "Create-Key-Value page description", + }, + "keyvalue.fieldName": { + message: "이름", + description: "Create-Key-Value form field label (store name)", + }, + "keyvalue.fieldNamePlaceholder": { + message: "example-key-value-name", + description: "Create-Key-Value name input placeholder", + }, + "keyvalue.fieldNameError": { + message: "소문자, 숫자, 하이픈만 사용하세요. 문자로 시작해야 합니다.", + description: "Create-Key-Value name validation message", + }, + "keyvalue.fieldPlan": { + message: "인스턴스 유형", + description: "Create-Key-Value form field label (plan / tier)", + }, + "keyvalue.fieldVersion": { + message: "Valkey 버전", + description: "Create-Key-Value form field label (major version)", + }, + "keyvalue.fieldVersionDefault": { + message: "기본값(최신)", + description: "Create-Key-Value version select default option", + }, + "keyvalue.fieldPublic": { + message: "공개 액세스", + description: "Create-Key-Value form field label (external endpoint toggle)", + }, + "keyvalue.fieldPublicHint": { + message: "TLS를 통해 클러스터 외부에서의 연결을 허용합니다.", + description: "Create-Key-Value public toggle helper text", + }, + "keyvalue.createCancel": { + message: "취소", + description: "Create-Key-Value page cancel button", + }, + "keyvalue.createSubmit": { + message: "키-값 인스턴스 생성", + description: "Create-Key-Value page submit button", + }, + "keyvalue.createSuccess": { + message: "{name} 생성 중…", + description: + "Toast after a create request is accepted (provisioning is async)", + }, + "keyvalue.createError": { + message: "{name} 생성하지 못했습니다. 다시 시도해 주세요.", + description: "Toast when a create request fails", + }, + // --- Detail metadata --- + "keyvalue.metaTitle": { + message: "상세 정보", + description: "Key Value detail metadata card title", + }, + "keyvalue.metaStatus": { + message: "상태", + description: "Key Value detail metadata row label", + }, + "keyvalue.metaPlan": { + message: "인스턴스 유형", + description: "Key Value detail metadata row label", + }, + "keyvalue.metaVersion": { + message: "버전", + description: "Key Value detail metadata row label", + }, + "keyvalue.metaPublic": { + message: "공개 액세스", + description: "Key Value detail metadata row label (external endpoint)", + }, + "keyvalue.metaExternalHost": { + message: "외부 호스트", + description: "Key Value detail metadata row label (SNI hostname)", + }, + "keyvalue.metaCreated": { + message: "생성일", + description: "Key Value detail metadata row label (relative age)", + }, + "keyvalue.yes": { + message: "예", + description: "Metadata value for a true boolean field", + }, + "keyvalue.no": { + message: "아니요", + description: "Metadata value for a false boolean field", + }, + "keyvalue.notFoundTitle": { + message: "키-값 저장소를 찾을 수 없습니다", + description: "Detail page state when keyValue(id) returns nothing", + }, + "keyvalue.notFoundBody": { + message: "{name} 이름의 키-값 저장소가 없거나 접근 권한이 없습니다.", + description: "Detail page not-found body", + }, + // --- Connection info panel --- + "keyvalue.connTitle": { + message: "연결", + description: "Connection-info panel card title", + }, + "keyvalue.connDescription": { + message: + "이 키-값 저장소의 연결 문자열입니다. 요청할 때만 표시되며 자동으로 노출되지 않습니다.", + description: "Connection-info panel card description", + }, + "keyvalue.connReveal": { + message: "연결 정보 표시", + description: "Button that fetches the connection info on demand", + }, + "keyvalue.connHide": { + message: "연결 정보 숨기기", + description: "Button that clears the revealed connection info", + }, + "keyvalue.connInternal": { + message: "내부 키-값 URL", + description: "Connection-info field label (in-cluster redis:// URL)", + }, + "keyvalue.connExternal": { + message: "외부 키-값 URL", + description: "Connection-info field label (public rediss:// URL)", + }, + "keyvalue.connExternalUnavailable": { + message: + "공개되지 않았습니다. 외부 URL을 받으려면 공개 액세스를 활성화하세요.", + description: + "Shown instead of the external URL when the store isn't public", + }, + "keyvalue.connCli": { + message: "Valkey CLI 명령어", + description: "Connection-info field label (ready-to-run redis-cli command)", + }, + "keyvalue.connErrorTitle": { + message: "연결 정보를 불러오지 못했습니다", + description: "Connection-info panel error title", + }, + "keyvalue.connErrorBody": { + message: + "저장소가 아직 프로비저닝 중이거나 자격 증명을 볼 권한이 없을 수 있습니다.", + description: "Connection-info panel error body", + }, + "keyvalue.copied": { + message: "클립보드에 복사됨", + description: "Toast after copying a connection field", + }, + "keyvalue.copyError": { + message: "클립보드에 복사하지 못했습니다", + description: "Toast when clipboard copy fails", + }, + // --- Suspend / resume --- + "keyvalue.lifecycleTitle": { + message: "라이프사이클", + description: "Suspend/resume card title", + }, + "keyvalue.lifecycleDescription": { + message: "이 저장소를 일시 중지하여 규모를 0으로 줄이거나 재개하세요.", + description: "Suspend/resume card description", + }, + "keyvalue.actionSuspend": { + message: "키-값 인스턴스 일시 중지", + description: "Suspend action button label", + }, + "keyvalue.actionResume": { + message: "키-값 인스턴스 재개", + description: "Resume action button label", + }, + "keyvalue.confirmSuspendTitle": { + message: "{name}을(를) 일시 중지하시겠습니까?", + description: "Suspend-confirmation dialog title", + }, + "keyvalue.confirmSuspendBody": { + message: + "이 작업은 저장소 규모를 0으로 줄이고 활성 연결을 모두 끊습니다. 데이터는 보존되며 언제든 재개할 수 있습니다.", + description: "Suspend-confirmation dialog body", + }, + "keyvalue.confirmCancel": { + message: "취소", + description: "Suspend-confirmation dialog cancel button", + }, + "keyvalue.toastSuspendSuccess": { + message: "{name} 일시 중지 중…", + description: "Toast after a suspend request is accepted", + }, + "keyvalue.toastSuspendError": { + message: "{name} 일시 중지하지 못했습니다. 다시 시도해 주세요.", + description: "Toast when a suspend request fails", + }, + "keyvalue.toastResumeSuccess": { + message: "{name} 재개 중…", + description: "Toast after a resume request is accepted", + }, + "keyvalue.toastResumeError": { + message: "{name} 재개하지 못했습니다. 다시 시도해 주세요.", + description: "Toast when a resume request fails", + }, +}; + +export default koKeyValue; diff --git a/dashboard/src/features/logs/locales/ko.ts b/dashboard/src/features/logs/locales/ko.ts new file mode 100644 index 00000000..f4942af6 --- /dev/null +++ b/dashboard/src/features/logs/locales/ko.ts @@ -0,0 +1,69 @@ +import type { TranslationEntry } from "@/i18n"; + +const koLogs: Record = { + "logs.typeLabel": { + message: "로그 유형", + description: "Accessible label for the log-type filter dropdown", + }, + "logs.typeAll": { + message: "모든 로그", + description: "Log-type filter option: no type filter", + }, + "logs.typeApplication": { + message: "애플리케이션 로그", + description: "Log-type filter option: application (app) logs", + }, + "logs.typeRequest": { + message: "요청 로그", + description: + "Log-type filter option: request logs (empty on bex — no backend)", + }, + "logs.searchPlaceholder": { + message: "로그 검색", + description: "Placeholder for the log search box (text filter)", + }, + "logs.live": { + message: "실시간", + description: "Label for the live-tail on/off toggle", + }, + "logs.jumpToLatest": { + message: "최신으로 이동", + description: "Button to re-pin autoscroll to the newest log line", + }, + "logs.loading": { + message: "로그 불러오는 중…", + description: "Shown while the first historical page is loading", + }, + "logs.streaming": { + message: "실시간 — 새 로그 스트리밍 중", + description: "Status under the log list when the SSE tail is connected", + }, + "logs.paused": { + message: "실시간 로그 일시 중지됨", + description: "Status under the log list when live tail is toggled off", + }, + "logs.disconnected": { + message: "실시간 로그 연결이 끊어졌습니다 — 재연결 중…", + description: "Banner when the SSE stream drops", + }, + "logs.emptyTitle": { + message: "아직 로그가 없습니다", + description: "Empty-state title when the service has produced no logs", + }, + "logs.emptyBody": { + message: "이 서비스는 아직 로그를 생성하지 않았습니다.", + description: "Empty-state body with no filters applied", + }, + "logs.emptyFilteredBody": { + message: + "이 필터와 일치하는 로그가 없습니다. bex는 애플리케이션 로그만 제공하며 요청 로그는 비어 있습니다.", + description: + "Empty-state body when a type/text filter yields nothing (honest about bex's app-only contract)", + }, + "logs.errorTitle": { + message: "로그를 불러오지 못했습니다", + description: "Error-state title when the logs query fails", + }, +}; + +export default koLogs; diff --git a/dashboard/src/features/metrics/locales/ko.ts b/dashboard/src/features/metrics/locales/ko.ts new file mode 100644 index 00000000..58fc82e7 --- /dev/null +++ b/dashboard/src/features/metrics/locales/ko.ts @@ -0,0 +1,126 @@ +import type { TranslationEntry } from "@/i18n"; + +const koMetrics: Record = { + "metrics.subtitle": { + message: "지표 — bex-api에서 실시간으로 제공", + description: "Subtitle on the service metrics page", + }, + "metrics.networkTitle": { + message: "네트워크 지표", + description: "Network metrics card title", + }, + "metrics.networkDescription": { + message: "모든 인스턴스에서 집계됨", + description: "Network metrics card description", + }, + "metrics.totalRequests": { + message: "총 요청 수", + description: "Network metrics chart section title", + }, + "metrics.responseTimes": { + message: "응답 시간 ({quantile})", + description: + "Network metrics chart section title, with the selected quantile", + }, + "metrics.outboundBandwidth": { + message: "아웃바운드 대역폭", + description: "Network metrics chart section title", + }, + "metrics.monthToDateBandwidth": { + message: "이번 달 사용량 {amount}", + description: + "Footer under the Outbound Bandwidth chart, showing month-to-date egress", + }, + "metrics.sourceNotConfigured": { + message: "지표 소스가 구성되지 않았습니다", + description: "Shown when bex-api reports no metrics backend is wired up", + }, + "metrics.applicationTitle": { + message: "애플리케이션 지표", + description: "Application metrics card title", + }, + "metrics.applicationDescription": { + message: "선택한 기간 동안의 인스턴스별 리소스 사용량", + description: "Application metrics card description", + }, + "metrics.limitLabel": { + message: "제한 {value}", + description: + "Resource limit annotation on the Memory/CPU chart headers, e.g. 'Limit 512 MiB'", + }, + "metrics.noLimitConfigured": { + message: "설정된 제한이 없습니다 — 백분율을 정의할 수 없습니다", + description: + "Shown instead of a percentage chart when the App's pods declare no resource limit", + }, + "metrics.statusCode": { + message: "상태 코드", + description: "Toolbar filter label for HTTP status code", + }, + "metrics.statusCodeAll": { + message: "전체", + description: "Status-code filter option meaning no filtering", + }, + "metrics.groupBy": { + message: "그룹화 기준", + description: "Total Requests chart control label", + }, + "metrics.groupByAllRequests": { + message: "모든 요청", + description: "Group-by option: one aggregate series", + }, + "metrics.groupByStatus": { + message: "상태 코드", + description: "Group-by option: one series per HTTP status code", + }, + "metrics.groupByMethod": { + message: "메서드", + description: "Group-by option: one series per HTTP method", + }, + "metrics.memory": { + message: "메모리", + description: "Application metrics stat tile label", + }, + "metrics.cpu": { + message: "CPU", + description: "Application metrics stat tile label", + }, + "metrics.totalInstances": { + message: "전체 인스턴스", + description: "Application metrics stat tile label", + }, + "metrics.filterPercentage": { + message: "백분율", + description: "Metrics filter tab: show values as a percentage", + }, + "metrics.filterTotal": { + message: "합계", + description: "Metrics filter tab: show values as an absolute total", + }, + "metrics.rangeLast30Minutes": { + message: "최근 30분", + description: "Metrics time-range filter option", + }, + "metrics.rangeLastHour": { + message: "최근 1시간", + description: "Metrics time-range filter option", + }, + "metrics.rangeLast3Hours": { + message: "최근 3시간", + description: "Metrics time-range filter option", + }, + "metrics.rangeLast6Hours": { + message: "최근 6시간", + description: "Metrics time-range filter option", + }, + "metrics.rangeLast12Hours": { + message: "최근 12시간", + description: "Metrics time-range filter option", + }, + "metrics.rangeLastDay": { + message: "최근 하루", + description: "Metrics time-range filter option", + }, +}; + +export default koMetrics; diff --git a/dashboard/src/features/services/locales/ko.ts b/dashboard/src/features/services/locales/ko.ts new file mode 100644 index 00000000..ccbeacc6 --- /dev/null +++ b/dashboard/src/features/services/locales/ko.ts @@ -0,0 +1,955 @@ +import type { TranslationEntry } from "@/i18n"; + +const koServices: Record = { + "services.statTotal": { + message: "전체 서비스", + description: "Services page stat card label", + }, + "services.statRunning": { + message: "실행 중", + description: "Services page stat card label", + }, + "services.statSuspended": { + message: "일시 중지됨", + description: "Services page stat card label", + }, + "services.cardTitle": { + message: "서비스", + description: + "Services table card title, also used as the metrics page back-link", + }, + "services.colName": { + message: "이름", + description: "Services table column header", + }, + "services.colStatus": { + message: "상태", + description: "Services table column header", + }, + "services.colUrl": { + message: "URL", + description: "Services table column header", + }, + "services.colInstances": { + message: "인스턴스", + description: "Services table column header (replica count — bex-native)", + }, + "services.colRevision": { + message: "리비전", + description: "Services table column header (active revision — bex-native)", + }, + "services.colCreated": { + message: "생성일", + description: "Services table column header (relative age from createdAt)", + }, + "services.colActions": { + message: "작업", + description: "Services table actions column header (screen-reader only)", + }, + "services.statusRunning": { + message: "실행 중", + description: "Services table status badge", + }, + "services.statusSuspended": { + message: "일시 중지됨", + description: "Services table status badge", + }, + "services.statusSleeping": { + message: "휴면 중", + description: + "Services status badge: a free-tier App auto-hibernated after idle (bex extension)", + }, + "services.statusSleepingHint": { + message: "리소스 절약을 위해 휴면 중입니다 — 다음 요청 시 깨어납니다.", + description: + "Hint next to the Sleeping badge explaining free-tier auto-sleep + wake-on-request", + }, + "services.statusPending": { + message: "대기 중", + description: "Services table status badge", + }, + "services.statusBuilding": { + message: "빌드 중", + description: "Services table status badge", + }, + "services.statusDeploying": { + message: "배포 중", + description: "Services table status badge", + }, + "services.statusFailed": { + message: "실패", + description: "Services table status badge", + }, + "services.statusUnknown": { + message: "알 수 없음", + description: "Services table status badge for an unrecognized phase", + }, + "services.actionsMenu": { + message: "작업 메뉴 열기", + description: "Accessible label for the per-row actions trigger", + }, + "services.actionSuspend": { + message: "일시 중지", + description: "Row action: park the service", + }, + "services.actionResume": { + message: "재개", + description: "Row action: bring a suspended service back", + }, + "services.actionRestart": { + message: "재시작", + description: "Row action: roll the service's pods", + }, + "services.confirmSuspendTitle": { + message: "{name}을(를) 일시 중지하시겠습니까?", + description: "Suspend confirmation dialog title", + }, + "services.confirmSuspendBody": { + message: + "서비스가 0으로 축소되어 트래픽 처리를 중단합니다. URL과 인증서는 유지되며 언제든 다시 재개할 수 있습니다.", + description: "Suspend confirmation dialog body", + }, + "services.confirmRestartTitle": { + message: "{name}을(를) 재시작하시겠습니까?", + description: "Restart confirmation dialog title", + }, + "services.confirmRestartBody": { + message: + "서비스의 파드가 다운타임 없이 순차적으로 교체됩니다. 진행 중인 요청은 기존 인스턴스가 교체되기 전에 완료됩니다.", + description: "Restart confirmation dialog body", + }, + "services.confirmCancel": { + message: "취소", + description: "Confirmation dialog cancel button", + }, + "services.toastSuspendSuccess": { + message: "{name} 일시 중지 중…", + description: "Toast shown after a suspend request is accepted", + }, + "services.toastResumeSuccess": { + message: "{name} 재개 중…", + description: "Toast shown after a resume request is accepted", + }, + "services.toastRestartSuccess": { + message: "{name} 재시작 중…", + description: "Toast shown after a restart request is accepted", + }, + "services.toastError": { + message: "{name}을(를) 업데이트하지 못했습니다. 다시 시도해 주세요.", + description: "Toast shown when a lifecycle action fails", + }, + "services.errorTitle": { + message: "서비스를 불러오지 못했습니다", + description: "Services list error card title", + }, + "services.errorBody": { + message: "bex-api 요청이 실패했습니다. 연결을 확인하고 다시 시도하세요.", + description: "Services list error card body", + }, + "services.emptyTitle": { + message: "아직 서비스가 없습니다", + description: "Services list empty state title", + }, + "services.emptyBody": { + message: "첫 번째 앱을 배포하면 여기에 표시됩니다.", + description: "Services list empty state body", + }, + "services.navLabel": { + message: "서비스 탐색", + description: "Accessible label for the service-detail tab nav", + }, + "services.navOverview": { + message: "개요", + description: "Service-detail nav item + overview panel title", + }, + "services.navLogs": { + message: "로그", + description: "Service-detail nav item (logs tab)", + }, + "services.navMetrics": { + message: "지표", + description: "Service-detail nav item (metrics tab)", + }, + "services.overviewPhase": { + message: "단계", + description: "Overview panel field label (operator phase, verbatim)", + }, + "services.overviewSuspended": { + message: "일시 중지됨", + description: "Overview panel field label (suspend state)", + }, + "services.overviewYes": { + message: "예", + description: "Overview panel value for a true boolean field", + }, + "services.overviewNo": { + message: "아니요", + description: "Overview panel value for a false boolean field", + }, + "services.notFoundTitle": { + message: "서비스를 찾을 수 없습니다", + description: "Overview page state when server(id) returns nothing", + }, + "services.notFoundBody": { + message: "{name} 이름의 서비스가 없거나 접근 권한이 없습니다.", + description: "Overview page not-found body", + }, + "services.notFoundBackToList": { + message: "서비스 목록으로 돌아가기", + description: + "Link on the service-detail not-found state back to the services list", + }, + "services.navEnvironment": { + message: "환경 변수", + description: "Service-detail nav item (environment variables tab)", + }, + "services.envTitle": { + message: "환경 변수", + description: "Environment tab card title", + }, + "services.envDescription": { + message: + "환경별 설정과 비밀 값을 지정한 뒤 코드에서 해당 값을 읽어 사용하세요.", + description: "Environment tab card description", + }, + "services.envColKey": { + message: "키", + description: "Environment table column header (variable name)", + }, + "services.envColValue": { + message: "값", + description: "Environment table column header (variable value)", + }, + "services.envShowSecret": { + message: "값 표시", + description: "Environment row button to reveal a masked value", + }, + "services.envHideSecret": { + message: "값 숨기기", + description: "Environment row button to re-mask a revealed value", + }, + "services.envRevealError": { + message: "값을 불러오지 못했습니다.", + description: "Environment row inline error when a value reveal fails", + }, + "services.envEmptyTitle": { + message: "환경 변수가 없습니다", + description: "Environment tab empty-state title", + }, + "services.envEmptyBody": { + message: "이 서비스를 구성하려면 변수를 추가하세요.", + description: "Environment tab empty-state body", + }, + "services.envUnavailableTitle": { + message: "환경 변수를 사용할 수 없습니다", + description: + "Environment tab state when the secret store is unconfigured (503)", + }, + "services.envUnavailableBody": { + message: "이 배포에는 비밀 저장소가 구성되어 있지 않습니다.", + description: "Environment tab unavailable-state body", + }, + "services.envForbiddenTitle": { + message: "권한 없음", + description: "Environment tab state when the caller lacks permission (403)", + }, + "services.envForbiddenBody": { + message: "이 서비스의 환경 변수를 볼 권한이 없습니다.", + description: "Environment tab forbidden-state body", + }, + "services.envErrorTitle": { + message: "환경 변수를 불러오지 못했습니다", + description: "Environment tab generic error title", + }, + "services.envErrorBody": { + message: "문제가 발생했습니다. 다시 시도해 주세요.", + description: "Environment tab generic error body", + }, + "services.envAdd": { + message: "변수 추가", + description: "Environment tab button to open the add-variable form", + }, + "services.envEdit": { + message: "수정", + description: "Environment row button to edit a variable's value", + }, + "services.envDelete": { + message: "삭제", + description: "Environment row button to remove a variable", + }, + "services.envSave": { + message: "저장", + description: "Environment add/edit form save button", + }, + "services.envCancel": { + message: "취소", + description: "Environment add/edit form cancel button", + }, + "services.envKeyPlaceholder": { + message: "변수_이름", + description: "Environment add-variable key input placeholder", + }, + "services.envValuePlaceholder": { + message: "값", + description: "Environment value input placeholder", + }, + "services.envInvalidKey": { + message: "문자, 숫자, 밑줄을 사용하세요. 숫자로 시작할 수 없습니다.", + description: + "Environment add-variable validation message for an invalid key", + }, + "services.envDeleteConfirmTitle": { + message: "{key}를 제거하시겠습니까?", + description: "Environment delete-confirmation dialog title", + }, + "services.envDeleteConfirmBody": { + message: "이 변수 없이 서비스가 다시 배포됩니다.", + description: "Environment delete-confirmation dialog body", + }, + "services.envRolloutNote": { + message: "변경 사항을 적용하기 위해 서비스가 다시 배포되고 있습니다.", + description: + "Toast description after an env-var write (bex rolls the pods)", + }, + "services.envSaveSuccess": { + message: "{key} 저장됨", + description: "Toast on a successful env-var add/update", + }, + "services.envSaveError": { + message: "{key} 저장하지 못했습니다", + description: "Toast on a failed env-var add/update", + }, + "services.envDeleteSuccess": { + message: "{key} 제거됨", + description: "Toast on a successful env-var delete", + }, + "services.envDeleteError": { + message: "{key} 제거하지 못했습니다", + description: "Toast on a failed env-var delete", + }, + "services.secretFilesTitle": { + message: "비밀 파일", + description: "Environment tab secret-files section title", + }, + "services.secretFilesDescription": { + message: + "배포 시 이 서비스에 마운트되는 비밀 내용(인증서, 자격 증명)을 담은 파일을 저장하세요.", + description: "Environment tab secret-files section description", + }, + "services.secretFileColName": { + message: "파일 이름", + description: "Secret-files table column header (file name)", + }, + "services.secretFileColContent": { + message: "내용", + description: "Secret-files table column header (file body)", + }, + "services.secretFilesEmptyTitle": { + message: "비밀 파일이 없습니다", + description: "Secret-files empty-state title", + }, + "services.secretFilesEmptyBody": { + message: "이 서비스에 비밀 내용을 마운트하려면 파일을 추가하세요.", + description: "Secret-files empty-state body", + }, + "services.secretFilesUnavailableTitle": { + message: "비밀 파일을 사용할 수 없습니다", + description: + "Secret-files state when the secret store is unconfigured (503)", + }, + "services.secretFilesUnavailableBody": { + message: "이 배포에는 비밀 저장소가 구성되어 있지 않습니다.", + description: "Secret-files unavailable-state body", + }, + "services.secretFilesForbiddenTitle": { + message: "권한 없음", + description: "Secret-files state when the caller lacks permission (403)", + }, + "services.secretFilesForbiddenBody": { + message: "이 서비스의 비밀 파일을 볼 권한이 없습니다.", + description: "Secret-files forbidden-state body", + }, + "services.secretFilesErrorTitle": { + message: "비밀 파일을 불러오지 못했습니다", + description: "Secret-files generic error title", + }, + "services.secretFilesErrorBody": { + message: "문제가 발생했습니다. 다시 시도해 주세요.", + description: "Secret-files generic error body", + }, + "services.secretFileAdd": { + message: "비밀 파일 추가", + description: "Secret-files button to open the add-file form", + }, + "services.secretFileNamePlaceholder": { + message: "파일명.확장자", + description: "Secret-files add-file name input placeholder", + }, + "services.secretFileContentPlaceholder": { + message: "파일 내용", + description: "Secret-files content input placeholder", + }, + "services.secretFileInvalidName": { + message: + "문자, 숫자, 점, 하이픈, 밑줄을 사용하세요. '.'이나 '..'은 사용할 수 없습니다.", + description: "Secret-files add-file validation message for an invalid name", + }, + "services.secretFileDeleteConfirmTitle": { + message: "{name}을(를) 제거하시겠습니까?", + description: "Secret-file delete-confirmation dialog title", + }, + "services.secretFileDeleteConfirmBody": { + message: "이 파일 없이 서비스가 다시 배포됩니다.", + description: "Secret-file delete-confirmation dialog body", + }, + "services.secretFileSaveSuccess": { + message: "{name} 저장됨", + description: "Toast on a successful secret-file add/update", + }, + "services.secretFileSaveError": { + message: "{name} 저장하지 못했습니다", + description: "Toast on a failed secret-file add/update", + }, + "services.secretFileDeleteSuccess": { + message: "{name} 제거됨", + description: "Toast on a successful secret-file delete", + }, + "services.secretFileDeleteError": { + message: "{name} 제거하지 못했습니다", + description: "Toast on a failed secret-file delete", + }, + "services.envGroupsTitle": { + message: "환경 그룹", + description: "Environment tab env-groups section title", + }, + "services.envGroupsDescription": { + message: + "이 서비스와 다른 서비스에 연결할 수 있는 재사용 가능한 환경 변수 및 비밀 파일 묶음입니다.", + description: "Environment tab env-groups section description", + }, + "services.envGroupsEmptyTitle": { + message: "환경 그룹이 없습니다", + description: "Env-groups empty-state title", + }, + "services.envGroupsEmptyBody": { + message: "서비스 간에 설정을 공유하려면 그룹을 만드세요.", + description: "Env-groups empty-state body", + }, + "services.envGroupsUnavailableTitle": { + message: "환경 그룹을 사용할 수 없습니다", + description: "Env-groups state when the secret store is unconfigured (503)", + }, + "services.envGroupsUnavailableBody": { + message: "이 배포에는 비밀 저장소가 구성되어 있지 않습니다.", + description: "Env-groups unavailable-state body", + }, + "services.envGroupsForbiddenTitle": { + message: "권한 없음", + description: "Env-groups state when the caller lacks permission (403)", + }, + "services.envGroupsForbiddenBody": { + message: "환경 그룹을 볼 권한이 없습니다.", + description: "Env-groups forbidden-state body", + }, + "services.envGroupsErrorTitle": { + message: "환경 그룹을 불러오지 못했습니다", + description: "Env-groups generic error title", + }, + "services.envGroupsErrorBody": { + message: "문제가 발생했습니다. 다시 시도해 주세요.", + description: "Env-groups generic error body", + }, + "services.envGroupCreate": { + message: "그룹 생성", + description: "Env-groups button to open the create-group form", + }, + "services.envGroupCreateSubmit": { + message: "생성", + description: "Env-groups create-group form submit button", + }, + "services.envGroupNamePlaceholder": { + message: "group-name", + description: "Env-groups create-group name input placeholder", + }, + "services.envGroupNameLabel": { + message: "그룹 이름", + description: "Env-groups create-group name input accessible label", + }, + "services.envGroupInvalidName": { + message: "문자, 숫자, 점, 하이픈, 밑줄을 사용하세요.", + description: + "Env-groups create-group validation message for an invalid name", + }, + "services.envGroupLinked": { + message: "연결됨", + description: + "Env-groups badge: this group is linked to the current service", + }, + "services.envGroupEmptyContents": { + message: "아직 변수나 파일이 없습니다.", + description: "Env-groups: shown when a group has no vars or secret files", + }, + "services.envGroupLink": { + message: "연결", + description: "Env-groups button: attach this group to the current service", + }, + "services.envGroupUnlink": { + message: "연결 해제", + description: + "Env-groups button: detach this group from the current service", + }, + "services.envGroupDelete": { + message: "삭제", + description: "Env-groups action: delete the group", + }, + "services.envGroupDeleteConfirmTitle": { + message: "{name}을(를) 삭제하시겠습니까?", + description: "Env-group delete-confirmation dialog title", + }, + "services.envGroupDeleteConfirmBody": { + message: "이 그룹은 연결된 모든 서비스에서 제거됩니다. 되돌릴 수 없습니다.", + description: "Env-group delete-confirmation dialog body", + }, + "services.envGroupCreateSuccess": { + message: "{name} 생성됨", + description: "Toast on a successful env-group create", + }, + "services.envGroupCreateError": { + message: "{name} 생성하지 못했습니다", + description: "Toast on a failed env-group create", + }, + "services.envGroupDeleteSuccess": { + message: "그룹이 삭제되었습니다", + description: "Toast on a successful env-group delete", + }, + "services.envGroupDeleteError": { + message: "그룹을 삭제하지 못했습니다", + description: "Toast on a failed env-group delete", + }, + "services.envGroupLinkSuccess": { + message: "그룹이 연결되었습니다", + description: "Toast on a successful env-group link", + }, + "services.envGroupLinkError": { + message: "그룹을 연결하지 못했습니다", + description: "Toast on a failed env-group link", + }, + "services.envGroupUnlinkSuccess": { + message: "그룹 연결이 해제되었습니다", + description: "Toast on a successful env-group unlink", + }, + "services.envGroupUnlinkError": { + message: "그룹 연결을 해제하지 못했습니다", + description: "Toast on a failed env-group unlink", + }, + "services.navSettings": { + message: "설정", + description: "Service-detail nav item (settings tab)", + }, + "services.settingsTitle": { + message: "설정", + description: "Settings tab card title", + }, + "services.settingsDescription": { + message: "이 서비스의 인스턴스 크기 및 기타 설정을 구성하세요.", + description: "Settings tab card description", + }, + "services.settingsInstanceType": { + message: "인스턴스 유형", + description: "Settings tab row label for the App's current plan/tier", + }, + "services.settingsNoInstanceType": { + message: "설정된 인스턴스 유형이 없습니다", + description: "Settings tab state for an untiered (bare-CR) App", + }, + "services.settingsUpdate": { + message: "업데이트", + description: "Settings tab link to the instance-type picker", + }, + "services.settingsIdleTimeout": { + message: "유휴 시간 제한", + description: + "Settings tab: label for the free-tier auto-sleep window control", + }, + "services.settingsIdleTimeoutHint": { + message: + "무료 서비스는 이 유휴 시간이 지나면 휴면 상태가 되며, 다음 요청 시 다시 깨어납니다.", + description: "Settings tab: idle-timeout control help text (bex extension)", + }, + "services.settingsIdleTimeoutPaid": { + message: "유료 서비스는 항상 켜져 있으며 절대 휴면 상태가 되지 않습니다.", + description: "Settings tab: shown instead of the control on a paid plan", + }, + "services.idleTimeoutDefault": { + message: "플랫폼 기본값", + description: "Idle-timeout option: 0 seconds = the operator's own window", + }, + "services.idleTimeoutMinutes": { + message: "{minutes}분", + description: "Idle-timeout option label in minutes", + }, + "services.idleTimeoutHours": { + message: "{hours}시간", + description: "Idle-timeout option label in hours", + }, + "services.idleTimeoutSeconds": { + message: "{seconds}초", + description: "Idle-timeout option label in seconds (non-round values)", + }, + "services.idleTimeoutSuccess": { + message: "유휴 시간 제한이 업데이트되었습니다.", + description: "Toast after setIdleTimeout succeeds", + }, + "services.idleTimeoutError": { + message: "유휴 시간 제한을 업데이트하지 못했습니다.", + description: "Toast after setIdleTimeout fails", + }, + "services.planPickerTitle": { + message: "인스턴스 유형 선택", + description: "Plan-picker page heading", + }, + "services.planPickerFreeGroup": { + message: "무료", + description: + "Plan-picker section label separating the Free tier from paid tiers", + }, + "services.planPickerPaidGroup": { + message: "유료", + description: "Plan-picker section label for the paid tier ladder", + }, + "services.planPickerCancel": { + message: "취소", + description: "Plan-picker footer button: discard the selection", + }, + "services.planPickerSave": { + message: "변경 사항 저장", + description: "Plan-picker footer button: confirm the plan change", + }, + "services.planPickerConfirmTitle": { + message: "인스턴스 유형을 {name}(으)로 변경하시겠습니까?", + description: "Plan-change confirm dialog title", + }, + "services.planPickerConfirmBody": { + message: + "서비스가 다운타임 없이 크기를 조정하고 순차적으로 교체됩니다 — 진행 중인 요청은 기존 인스턴스가 교체되기 전에 완료됩니다.", + description: "Plan-change confirm dialog body", + }, + "services.planPickerSuccess": { + message: "인스턴스 유형이 {name}(으)로 업데이트되었습니다", + description: "Toast on a successful plan change", + }, + "services.planPickerError": { + message: "인스턴스 유형을 업데이트하지 못했습니다. 다시 시도해 주세요.", + description: "Toast on a failed plan change", + }, + "services.planPickerErrorTitle": { + message: "인스턴스 유형을 불러오지 못했습니다", + description: "Plan-picker error state title (instanceTypes query failed)", + }, + "services.planPickerErrorBody": { + message: "bex-api 요청이 실패했습니다. 연결을 확인하고 다시 시도하세요.", + description: "Plan-picker error state body", + }, + "services.domainsTitle": { + message: "커스텀 도메인", + description: "Settings tab custom-domains section title", + }, + "services.domainsDescription": { + message: "소유한 커스텀 도메인을 이 서비스로 연결하세요.", + description: "Settings tab custom-domains section description", + }, + "services.domainColName": { + message: "이름", + description: "Custom-domains table column header (the FQDN)", + }, + "services.domainColVerified": { + message: "확인 상태", + description: + "Custom-domains table column header (DNS/ownership verification)", + }, + "services.domainColCertificate": { + message: "인증서 상태", + description: + "Custom-domains table column header (TLS certificate serving state)", + }, + "services.domainColActions": { + message: "작업", + description: + "Custom-domains table actions column header (screen-reader only)", + }, + "services.domainVerified": { + message: "확인됨", + description: "Custom-domains status badge: TLS certificate has been issued", + }, + "services.domainCertActive": { + message: "활성", + description: + "Custom-domains status badge: certificate issued and serving traffic", + }, + "services.domainPending": { + message: "대기 중", + description: + "Custom-domains status badge: certificate not yet issued/serving", + }, + "services.domainActionsMenu": { + message: "도메인 작업 메뉴 열기", + description: "Accessible label for the per-domain actions trigger", + }, + "services.domainDelete": { + message: "삭제", + description: "Custom-domains row action: remove the domain", + }, + "services.domainCancel": { + message: "취소", + description: "Custom-domains dialog cancel button", + }, + "services.domainDeleteConfirmTitle": { + message: "{name}을(를) 삭제하시겠습니까?", + description: "Custom-domain delete-confirmation dialog title", + }, + "services.domainDeleteConfirmBody": { + message: + "서비스가 이 도메인에 대한 처리를 중단합니다. Ingress 규칙이 제거되고 TLS 인증서는 만료되도록 방치됩니다. 되돌릴 수 없습니다.", + description: "Custom-domain delete-confirmation dialog body", + }, + "services.domainAdd": { + message: "커스텀 도메인 추가", + description: "Custom-domains button to open the add-domain dialog", + }, + "services.domainAddTitle": { + message: "커스텀 도메인 추가", + description: "Add-domain dialog title", + }, + "services.domainAddDescription": { + message: + "소유한 도메인을 입력하세요. DNS를 이 서비스로 연결하면 bex가 자동으로 TLS 인증서를 발급합니다.", + description: "Add-domain dialog description", + }, + "services.domainPlaceholder": { + message: "www.example.com", + description: "Add-domain FQDN input placeholder", + }, + "services.domainInvalid": { + message: "유효한 도메인을 입력하세요. 예: www.example.com", + description: "Add-domain validation message for a malformed hostname", + }, + "services.domainAddButton": { + message: "도메인 추가", + description: "Add-domain dialog submit button", + }, + "services.domainAddSuccess": { + message: "{name} 추가됨", + description: "Toast on a successful custom-domain add", + }, + "services.domainAddError": { + message: "{name} 추가하지 못했습니다", + description: "Toast on a failed custom-domain add", + }, + "services.domainDeleteSuccess": { + message: "{name} 제거됨", + description: "Toast on a successful custom-domain delete", + }, + "services.domainDeleteError": { + message: "{name} 제거하지 못했습니다", + description: "Toast on a failed custom-domain delete", + }, + "services.domainPropagateNote": { + message: "DNS와 TLS 인증서가 백그라운드에서 전파됩니다.", + description: + "Toast description after a custom-domain add (async convergence)", + }, + "services.domainDnsToggle": { + message: "DNS 설정 표시", + description: + "aria-label for the per-domain DNS-instructions disclosure toggle", + }, + "services.domainDnsTitle": { + message: "DNS 설정", + description: "Heading of the per-domain DNS-instructions panel", + }, + "services.domainDnsSubdomainGuidance": { + message: + "DNS 공급자에서 다음 레코드를 생성한 뒤 다시 확인하세요. 확인이 완료되면 bex가 자동으로 TLS 인증서를 발급합니다.", + description: "Guidance line above the DNS record for a subdomain", + }, + "services.domainDnsApexGuidance": { + message: + "최상위(apex) 도메인은 일반 CNAME을 사용할 수 없습니다. 공급자가 ALIAS/ANAME(또는 CNAME 플래트닝)을 지원하면 이 레코드를 생성하고, 그렇지 않다면 등록기관에서 apex를 www 서브도메인으로 리디렉션하세요.", + description: "Guidance line above the DNS record for an apex domain", + }, + "services.domainRecordType": { + message: "유형", + description: "Label for the DNS record type field (CNAME/ALIAS)", + }, + "services.domainRecordHost": { + message: "호스트", + description: "Label for the DNS record host/name field", + }, + "services.domainRecordTarget": { + message: "대상", + description: "Label for the DNS record target/value field", + }, + "services.domainDnsUnavailable": { + message: + "DNS 대상을 아직 사용할 수 없습니다 — 서비스가 실행되면 다시 확인하세요.", + description: "Shown when the backend couldn't derive the DNS record target", + }, + "services.domainRecheck": { + message: "다시 확인", + description: "Button that re-checks a domain's DNS/certificate status", + }, + "services.domainCopied": { + message: "클립보드에 복사됨", + description: "Toast when a DNS record value is copied", + }, + "services.domainCopyError": { + message: "클립보드에 복사하지 못했습니다", + description: "Toast when copying a DNS record value fails", + }, + "services.domainAddedTitle": { + message: "도메인이 추가되었습니다 — DNS를 설정하세요", + description: "Title of the post-add DNS-record step in the add dialog", + }, + "services.domainAddedDescription": { + message: "도메인 연결을 완료하려면 DNS 공급자에서 이 레코드를 생성하세요.", + description: "Subtitle of the post-add DNS-record step in the add dialog", + }, + "services.domainDone": { + message: "완료", + description: "Button closing the post-add DNS-record step", + }, + "services.domainVerifySuccess": { + message: "{name} 확인됨", + description: "Toast when a re-check finds the domain verified", + }, + "services.domainVerifyPending": { + message: + "{name}이(가) 아직 확인되지 않았습니다 — DNS가 전파되는 중일 수 있습니다.", + description: "Toast when a re-check finds the domain still pending", + }, + "services.domainVerifyError": { + message: "{name}을(를) 다시 확인하지 못했습니다.", + description: "Toast when the re-check request fails", + }, + "services.domainsEmptyTitle": { + message: "커스텀 도메인이 없습니다", + description: "Custom-domains empty-state title", + }, + "services.domainsEmptyBody": { + message: "이 서비스에서 제공하려면 소유한 도메인을 추가하세요.", + description: "Custom-domains empty-state body", + }, + "services.domainsErrorTitle": { + message: "커스텀 도메인을 불러오지 못했습니다", + description: "Custom-domains generic error title", + }, + "services.domainsErrorBody": { + message: "bex-api 요청이 실패했습니다. 연결을 확인하고 다시 시도하세요.", + description: "Custom-domains generic error body", + }, + "services.platformSubdomainTitle": { + message: "플랫폼 서브도메인", + description: "Settings tab platform-subdomain section title", + }, + "services.platformSubdomainDescription": { + message: + "이 서비스는 커스텀 도메인 외에도 항상 bex 플랫폼 서브도메인으로 접근할 수 있습니다.", + description: "Settings tab platform-subdomain section description", + }, + "services.platformSubdomainEnabled": { + message: "항상 활성화됨", + description: "Platform-subdomain badge: the subdomain can't be turned off", + }, + "services.platformSubdomainPending": { + message: "서비스가 실행되면 플랫폼 URL이 할당됩니다.", + description: "Platform-subdomain state when the service has no URL yet", + }, + "services.deployTitle": { + message: "배포", + description: "Cron job Settings tab: Deploy section title (Render parity)", + }, + "services.deployDescription": { + message: "이 크론 작업이 실행되는 방식입니다 — 현재는 읽기 전용입니다.", + description: "Cron job Settings tab: Deploy section description", + }, + "services.deployScheduleLabel": { + message: "일정", + description: "Cron job Settings tab: Deploy section schedule field label", + }, + "services.deployScheduleHint": { + message: "이 일정(5필드 crontab)에 따라 이 명령을 실행합니다.", + description: "Cron job Settings tab: Deploy section schedule help text", + }, + "services.deployCommandLabel": { + message: "명령어", + description: "Cron job Settings tab: Deploy section command field label", + }, + "services.deployCommandEmpty": { + message: "이미지 자체의 기본 명령을 사용합니다.", + description: + "Cron job Settings tab: shown when spec.command is unset (no override)", + }, + "services.colType": { + message: "유형", + description: "Services table column header (service type)", + }, + "services.typeWeb": { + message: "웹 서비스", + description: "Service-type badge: an HTTP service exposed at a URL", + }, + "services.typePrivate": { + message: "비공개 서비스", + description: + "Service-type badge: an HTTP service reachable only in-cluster", + }, + "services.typeWorker": { + message: "백그라운드 워커", + description: "Service-type badge: runs with no HTTP port/URL", + }, + "services.typeCron": { + message: "크론 작업", + description: "Service-type badge: runs a command on a schedule", + }, + "services.typeUnknown": { + message: "서비스", + description: "Service-type badge fallback for an unrecognized type", + }, + "services.overviewType": { + message: "유형", + description: "Overview tab row label for the service type", + }, + "services.overviewSchedule": { + message: "일정", + description: "Overview tab row label for a cron job's schedule", + }, + "services.cronRunsTitle": { + message: "최근 실행 내역", + description: "Cron job overview: recent-runs section title", + }, + "services.cronRunsEmpty": { + message: "아직 실행 내역이 없습니다.", + description: "Cron job overview: shown when a cron has no run history", + }, + "services.cronRunColStarted": { + message: "시작 시각", + description: "Cron runs table column header (run start time)", + }, + "services.cronRunColStatus": { + message: "상태", + description: "Cron runs table column header (run outcome)", + }, + "services.cronRunStatusRunning": { + message: "실행 중", + description: "Cron run status badge", + }, + "services.cronRunStatusSucceeded": { + message: "성공", + description: "Cron run status badge", + }, + "services.cronRunStatusFailed": { + message: "실패", + description: "Cron run status badge", + }, +}; + +export default koServices; diff --git a/dashboard/src/features/team/locales/ko.ts b/dashboard/src/features/team/locales/ko.ts new file mode 100644 index 00000000..f2d5555b --- /dev/null +++ b/dashboard/src/features/team/locales/ko.ts @@ -0,0 +1,155 @@ +import type { TranslationEntry } from "@/i18n"; + +const koTeam: Record = { + "team.title": { + message: "팀", + description: "Settings Team section card title", + }, + "team.description": { + message: + "이 워크스페이스에 속한 사람들과 그들이 할 수 있는 작업입니다. 이메일로 팀원을 초대하고 역할을 지정하세요. 역할은 모든 리소스에 걸쳐 적용됩니다.", + description: "Settings Team section card description", + }, + "team.colMember": { + message: "멤버", + description: "Team table column header — the member identity", + }, + "team.colRole": { + message: "역할", + description: "Team table column header — the member's role", + }, + "team.colEmail": { + message: "이메일", + description: "Pending invites table column header", + }, + "team.pendingTitle": { + message: "대기 중인 초대", + description: "Heading above the pending-invites table", + }, + "team.invite": { + message: "초대", + description: "Button that opens the invite dialog", + }, + "team.inviteTitle": { + message: "팀원 초대", + description: "Invite dialog title", + }, + "team.inviteDescription": { + message: + "이메일을 받게 되며, 해당 주소로 처음 로그인하면 이 워크스페이스에 합류합니다.", + description: "Invite dialog description", + }, + "team.fieldEmail": { + message: "이메일 주소", + description: "Invite dialog email field label", + }, + "team.fieldEmailPlaceholder": { + message: "teammate@example.com", + description: "Invite dialog email field placeholder", + }, + "team.fieldRole": { + message: "역할", + description: "Invite dialog role field label", + }, + "team.inviteCancel": { + message: "취소", + description: "Invite dialog cancel button", + }, + "team.inviteSubmit": { + message: "초대 보내기", + description: "Invite dialog submit button", + }, + "team.inviteSuccess": { + message: "{email}에게 초대를 보냈습니다", + description: "Toast after a successful invite", + }, + "team.inviteError": { + message: "{email}을(를) 초대하지 못했습니다", + description: "Toast after a failed invite", + }, + "team.inviteErrorPlan": { + message: + "워크스페이스 플랜의 멤버 한도에 도달했습니다 — 더 초대하려면 업그레이드하세요.", + description: "Toast when the workspace plan's member cap blocks an invite", + }, + "team.remove": { + message: "제거", + description: "Remove-member button / confirm label", + }, + "team.removeTitle": { + message: "멤버를 제거하시겠습니까?", + description: "Remove-member confirmation dialog title", + }, + "team.removeConfirm": { + message: + "{subject}은(는) 이 워크스페이스에 대한 접근 권한을 즉시 잃게 됩니다.", + description: "Remove-member confirmation dialog body", + }, + "team.removeCancel": { + message: "취소", + description: "Remove-member confirmation cancel button", + }, + "team.removeSuccess": { + message: "멤버가 제거되었습니다", + description: "Toast after a successful remove", + }, + "team.removeError": { + message: "해당 멤버를 제거하지 못했습니다", + description: "Toast after a failed remove", + }, + "team.roleChanged": { + message: "역할이 업데이트되었습니다", + description: "Toast after a successful role change", + }, + "team.roleChangeError": { + message: "해당 역할을 변경하지 못했습니다", + description: "Toast after a failed role change", + }, + "team.lastAdminError": { + message: "워크스페이스에는 최소 한 명의 관리자가 있어야 합니다.", + description: "Toast when the last-admin guard refuses a demote/remove", + }, + "team.revokeInvite": { + message: "취소", + description: "Revoke pending-invite button", + }, + "team.revokeInviteSuccess": { + message: "초대가 취소되었습니다", + description: "Toast after a successful invite revoke", + }, + "team.revokeInviteError": { + message: "해당 초대를 취소하지 못했습니다", + description: "Toast after a failed invite revoke", + }, + "team.errorTitle": { + message: "팀을 불러오지 못했습니다", + description: "Team panel generic error title", + }, + "team.errorBody": { + message: + "이 워크스페이스의 멤버를 불러오는 중 문제가 발생했습니다. 다시 시도해 주세요.", + description: "Team panel generic error body", + }, + "team.role.VIEWER": { + message: "뷰어", + description: "Role label — read-only", + }, + "team.role.CONTRIBUTOR": { + message: "컨트리뷰터", + description: "Role label — works on resources, no sensitive fields", + }, + "team.role.DEVELOPER": { + message: "개발자", + description: "Role label — full resource access", + }, + "team.role.ADMIN": { + message: "관리자", + description: "Role label — full access incl. settings, members, billing", + }, + "team.role.BILLING": { + message: "결제 담당자", + description: "Role label — billing only", + }, +}; + +export default koTeam; diff --git a/dashboard/src/features/usage/locales/ko.ts b/dashboard/src/features/usage/locales/ko.ts new file mode 100644 index 00000000..fc9ee46b --- /dev/null +++ b/dashboard/src/features/usage/locales/ko.ts @@ -0,0 +1,70 @@ +import type { TranslationEntry } from "@/i18n"; + +const koUsage: Record = { + "usage.pageTitle": { + message: "사용량", + description: "Usage page heading and browser title", + }, + "usage.pageSubtitle": { + message: "이번 달 워크스페이스 사용량", + description: "Usage page subtitle beneath the heading", + }, + "usage.computeTitle": { + message: "컴퓨팅", + description: "Compute section heading on the Usage page", + }, + "usage.computeDescription": { + message: "서비스 및 플랜별 인스턴스 시간", + description: "Compute section description", + }, + "usage.bandwidthTitle": { + message: "대역폭", + description: "Bandwidth section heading on the Usage page", + }, + "usage.bandwidthDescription": { + message: "Traefik에서 계측한 아웃바운드 트래픽", + description: "Bandwidth section description", + }, + "usage.buildTitle": { + message: "빌드 시간", + description: "Build minutes section heading on the Usage page", + }, + "usage.buildDescription": { + message: "빌드에 소요된 파이프라인 시간(분)", + description: "Build section description", + }, + "usage.colService": { + message: "서비스", + description: "Table column header: service name", + }, + "usage.colPlan": { + message: "플랜", + description: "Table column header: service plan/tier", + }, + "usage.colHours": { + message: "시간", + description: "Table column header: compute hours", + }, + "usage.colBandwidth": { + message: "대역폭", + description: "Table column header: egress bandwidth", + }, + "usage.colMinutes": { + message: "분", + description: "Table column header: build minutes", + }, + "usage.totalRow": { + message: "합계", + description: "Summary row label in usage tables", + }, + "usage.empty": { + message: "이번 달 기록된 사용량이 없습니다.", + description: "Empty-state message when a section has no data", + }, + "usage.errorTitle": { + message: "사용량을 불러오지 못했습니다", + description: "Error state heading on the Usage page", + }, +}; + +export default koUsage; diff --git a/dashboard/src/features/workspaces/locales/ko.ts b/dashboard/src/features/workspaces/locales/ko.ts new file mode 100644 index 00000000..19d842f1 --- /dev/null +++ b/dashboard/src/features/workspaces/locales/ko.ts @@ -0,0 +1,185 @@ +import type { TranslationEntry } from "@/i18n"; + +const koWorkspaces: Record = { + "workspaces.switcherEmpty": { + message: "워크스페이스 선택", + description: "Switcher trigger label before any workspace is selected", + }, + "workspaces.switcherLabel": { + message: "워크스페이스", + description: "Switcher dropdown label above the workspace list", + }, + "workspaces.switcherSettings": { + message: "워크스페이스 설정", + description: "Switcher menu item linking to /workspace/settings", + }, + "workspaces.switcherNew": { + message: "+ 새 워크스페이스", + description: "Switcher menu item linking to /new/workspace", + }, + "workspaces.planPickerLabel": { + message: "플랜", + description: "Accessible label for the plan-card radiogroup", + }, + "workspaces.planHobbyName": { + message: "Hobby", + description: "Workspace plan card name", + }, + "workspaces.planHobbyPrice": { + message: "무료", + description: "Workspace plan card price", + }, + "workspaces.planHobbyDescription": { + message: "멤버 1명, 최대 25개 서비스, 사용자당 Hobby 워크스페이스 5개.", + description: "Workspace plan card description", + }, + "workspaces.planProName": { + message: "Pro", + description: "Workspace plan card name", + }, + "workspaces.planProPrice": { + message: "월 $25", + description: "Workspace plan card price", + }, + "workspaces.planProDescription": { + message: "무제한 멤버 및 서비스.", + description: "Workspace plan card description", + }, + "workspaces.planScaleName": { + message: "Scale", + description: "Workspace plan card name", + }, + "workspaces.planScalePrice": { + message: "월 $499", + description: "Workspace plan card price", + }, + "workspaces.planScaleDescription": { + message: "무제한 멤버 및 서비스, 추가 역할.", + description: "Workspace plan card description", + }, + "workspaces.planEnterpriseName": { + message: "Enterprise", + description: "Workspace plan card name", + }, + "workspaces.planEnterprisePrice": { + message: "맞춤형", + description: "Workspace plan card price", + }, + "workspaces.planEnterpriseDescription": { + message: "맞춤형 제한 및 지원.", + description: "Workspace plan card description", + }, + "workspaces.createTitle": { + message: "워크스페이스 생성", + description: "/new/workspace card title", + }, + "workspaces.createDescription": { + message: "이름을 지정하고 플랜을 선택하세요.", + description: "/new/workspace card description", + }, + "workspaces.fieldName": { + message: "이름", + description: "Workspace name field label (shared by create + settings)", + }, + "workspaces.fieldNamePlaceholder": { + message: "예: acme-staging", + description: "Workspace name field placeholder", + }, + "workspaces.fieldNameError": { + message: + "소문자, 숫자, 하이픈만 사용하며 1~30자여야 합니다. 하이픈으로 시작하거나 끝날 수 없습니다.", + description: "Workspace name validation error", + }, + "workspaces.fieldPlan": { + message: "플랜", + description: "Workspace plan field label (create picker + settings badge)", + }, + "workspaces.createErrorTitle": { + message: "워크스페이스를 생성하지 못했습니다", + description: "/new/workspace inline error alert title", + }, + "workspaces.createCancel": { + message: "취소", + description: "/new/workspace cancel button", + }, + "workspaces.createSubmit": { + message: "워크스페이스 생성", + description: "/new/workspace submit button", + }, + "workspaces.createSuccess": { + message: "{name} 생성됨", + description: "Toast on a successful workspace create", + }, + "workspaces.createError": { + message: "워크스페이스를 생성하지 못했습니다", + description: "Fallback toast/inline message on a failed create", + }, + "workspaces.settingsTitle": { + message: "워크스페이스", + description: "Workspace settings card title", + }, + "workspaces.settingsDescription": { + message: + "이 워크스페이스의 이름을 바꾸거나 플랜과 메타데이터를 확인하세요.", + description: "Workspace settings card description", + }, + "workspaces.settingsEmpty": { + message: "선택된 워크스페이스가 없습니다.", + description: "Workspace settings page empty state", + }, + "workspaces.renameSubmit": { + message: "저장", + description: "Workspace rename form submit button", + }, + "workspaces.renameErrorTitle": { + message: "워크스페이스 이름을 변경하지 못했습니다", + description: "Workspace rename inline error alert title", + }, + "workspaces.renameSuccess": { + message: "{name}(으)로 이름이 변경되었습니다", + description: "Toast on a successful rename", + }, + "workspaces.renameError": { + message: "워크스페이스 이름을 변경하지 못했습니다", + description: "Fallback toast/inline message on a failed rename", + }, + "workspaces.fieldId": { + message: "워크스페이스 ID", + description: "Workspace settings metadata field label", + }, + "workspaces.fieldCreatedAt": { + message: "생성일", + description: "Workspace settings metadata field label", + }, + "workspaces.dangerZoneTitle": { + message: "위험 구역", + description: "Workspace settings delete section title", + }, + "workspaces.dangerZoneDescription": { + message: + "워크스페이스를 삭제하면 서비스, 데이터베이스, 환경 변수가 영구적으로 제거됩니다. 되돌릴 수 없습니다.", + description: "Workspace settings delete section description", + }, + "workspaces.deleteConfirmLabel": { + message: "확인하려면 {name}을(를) 입력하세요", + description: "Delete-guard input label naming the exact workspace name", + }, + "workspaces.deleteErrorTitle": { + message: "워크스페이스를 삭제하지 못했습니다", + description: "Delete danger-zone inline error alert title", + }, + "workspaces.deleteSubmit": { + message: "워크스페이스 삭제", + description: "Delete danger-zone submit button", + }, + "workspaces.deleteSuccess": { + message: "{name} 삭제됨", + description: "Toast on a successful delete", + }, + "workspaces.deleteError": { + message: "워크스페이스를 삭제하지 못했습니다", + description: "Fallback toast/inline message on a failed delete", + }, +}; + +export default koWorkspaces; diff --git a/dashboard/src/i18n/__tests__/locale-parity.test.ts b/dashboard/src/i18n/__tests__/locale-parity.test.ts index 0b0ee6e7..5427b742 100644 --- a/dashboard/src/i18n/__tests__/locale-parity.test.ts +++ b/dashboard/src/i18n/__tests__/locale-parity.test.ts @@ -1,18 +1,22 @@ import { describe, it, expect } from "vitest"; import enCommon from "@/common/locales/en"; import zhCommon from "@/common/locales/zh"; +import koCommon from "@/common/locales/ko"; import enAuth from "@/features/auth/locales/en"; import zhAuth from "@/features/auth/locales/zh"; +import koAuth from "@/features/auth/locales/ko"; import enMetrics from "@/features/metrics/locales/en"; import zhMetrics from "@/features/metrics/locales/zh"; +import koMetrics from "@/features/metrics/locales/ko"; import enServices from "@/features/services/locales/en"; import zhServices from "@/features/services/locales/zh"; +import koServices from "@/features/services/locales/ko"; const NAMESPACES = [ - { name: "common", en: enCommon, zh: zhCommon }, - { name: "auth", en: enAuth, zh: zhAuth }, - { name: "metrics", en: enMetrics, zh: zhMetrics }, - { name: "services", en: enServices, zh: zhServices }, + { name: "common", en: enCommon, zh: zhCommon, ko: koCommon }, + { name: "auth", en: enAuth, zh: zhAuth, ko: koAuth }, + { name: "metrics", en: enMetrics, zh: zhMetrics, ko: koMetrics }, + { name: "services", en: enServices, zh: zhServices, ko: koServices }, ]; describe("locale key parity", () => { @@ -23,6 +27,13 @@ describe("locale key parity", () => { expect(zhKeys).toEqual(enKeys); }); + it.each(NAMESPACES)("$name: en and ko have the same keys", ({ en, ko }) => { + const enKeys = Object.keys(en).sort(); + const koKeys = Object.keys(ko).sort(); + + expect(koKeys).toEqual(enKeys); + }); + it.each(NAMESPACES)( "$name: every key is namespace-prefixed", ({ name, en }) => { @@ -34,8 +45,12 @@ describe("locale key parity", () => { it.each(NAMESPACES)( "$name: every entry has a non-empty message", - ({ en, zh }) => { - for (const entry of [...Object.values(en), ...Object.values(zh)]) { + ({ en, zh, ko }) => { + for (const entry of [ + ...Object.values(en), + ...Object.values(zh), + ...Object.values(ko), + ]) { expect(entry.message.length).toBeGreaterThan(0); } }, diff --git a/dashboard/src/i18n/config.ts b/dashboard/src/i18n/config.ts index c69ac1d8..4a8b66fd 100644 --- a/dashboard/src/i18n/config.ts +++ b/dashboard/src/i18n/config.ts @@ -1,4 +1,4 @@ -export const SUPPORTED_LANGUAGES = ["en", "zh"] as const; +export const SUPPORTED_LANGUAGES = ["en", "zh", "ko"] as const; export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]; @@ -7,6 +7,7 @@ export const DEFAULT_LANGUAGE: SupportedLanguage = "en"; export const LANGUAGE_NAMES: Record = { en: "English", zh: "中文", + ko: "한국어", }; /** Narrows an arbitrary string (URL param, cookie, header) to a supported language, or null. */ diff --git a/dashboard/src/i18n/index.ts b/dashboard/src/i18n/index.ts index 98d096fb..e18f0000 100644 --- a/dashboard/src/i18n/index.ts +++ b/dashboard/src/i18n/index.ts @@ -20,6 +20,17 @@ import zhApiKeys from "@/features/api-keys/locales/zh"; import zhWorkspaces from "@/features/workspaces/locales/zh"; import zhTeam from "@/features/team/locales/zh"; import zhUsage from "@/features/usage/locales/zh"; +import koCommon from "@/common/locales/ko"; +import koAuth from "@/features/auth/locales/ko"; +import koLogs from "@/features/logs/locales/ko"; +import koMetrics from "@/features/metrics/locales/ko"; +import koServices from "@/features/services/locales/ko"; +import koDatabases from "@/features/databases/locales/ko"; +import koKeyValue from "@/features/keyvalue/locales/ko"; +import koApiKeys from "@/features/api-keys/locales/ko"; +import koWorkspaces from "@/features/workspaces/locales/ko"; +import koTeam from "@/features/team/locales/ko"; +import koUsage from "@/features/usage/locales/ko"; import type { SupportedLanguage, TranslationEntry } from "./config"; export type { SupportedLanguage, TranslationEntry } from "./config"; @@ -73,10 +84,25 @@ export const zh: Record = { ...extractMessages(zhUsage), }; +export const ko: Record = { + ...extractMessages(koCommon), + ...extractMessages(koAuth), + ...extractMessages(koLogs), + ...extractMessages(koMetrics), + ...extractMessages(koServices), + ...extractMessages(koDatabases), + ...extractMessages(koKeyValue), + ...extractMessages(koApiKeys), + ...extractMessages(koWorkspaces), + ...extractMessages(koTeam), + ...extractMessages(koUsage), +}; + export const resources: Record< SupportedLanguage, { translation: Record } > = { en: { translation: en }, zh: { translation: zh }, + ko: { translation: ko }, };