From d1ea293b1ff87c5954379f14049ef3564f926ff7 Mon Sep 17 00:00:00 2001 From: JeonJunYeong Date: Mon, 26 Oct 2026 15:00:14 +0900 Subject: [PATCH] feat(i18n): add Korean language support to the web client Signed-off-by: JeonJunYeong --- README.md | 2 +- README_zh.md | 2 +- web/src/app/layout.tsx | 6 +- web/src/app/router.tsx | 4 +- .../notification/notification-content.test.ts | 40 + .../notification/notification-content.ts | 83 +- .../skill/skill-avatar-manager.test.tsx | 7 +- .../features/skill/skill-avatar-manager.tsx | 4 +- web/src/features/skill/skill-card.test.ts | 3 +- .../features/social/skill-reviews.test.tsx | 3 +- web/src/i18n/config.test.ts | 7 +- web/src/i18n/config.ts | 2 + ...ithub-deep-discovery-drawer-locale.test.ts | 3 +- web/src/i18n/github-sync-locale.test.ts | 3 + web/src/i18n/ko-locale.test.ts | 51 + .../i18n/landing-quick-start-locale.test.ts | 2 + web/src/i18n/language-detection.test.ts | 54 + web/src/i18n/locales/en.json | 57 + web/src/i18n/locales/ko-legal.json | 156 + web/src/i18n/locales/ko.json | 3582 +++++++++++++++++ web/src/i18n/locales/ru.json | 57 + web/src/i18n/locales/zh.json | 57 + web/src/i18n/security-audit-locale.test.ts | 3 + web/src/i18n/suite-locale.test.ts | 3 + web/src/index.css | 6 + web/src/pages/login.test.tsx | 34 +- web/src/pages/login.tsx | 26 +- web/src/pages/privacy.test.tsx | 32 +- web/src/pages/privacy.tsx | 5 +- web/src/pages/terms.test.tsx | 32 +- web/src/pages/terms.tsx | 5 +- .../components/language-switcher.test.tsx | 17 + .../shared/components/language-switcher.tsx | 1 + web/src/shared/lib/api-error.test.ts | 15 + web/src/shared/lib/api-error.ts | 9 +- web/src/shared/lib/document-language.test.ts | 9 + .../shared/lib/format-relative-time.test.ts | 35 + web/src/shared/lib/format-relative-time.ts | 36 +- 38 files changed, 4300 insertions(+), 153 deletions(-) create mode 100644 web/src/i18n/ko-locale.test.ts create mode 100644 web/src/i18n/language-detection.test.ts create mode 100644 web/src/i18n/locales/ko-legal.json create mode 100644 web/src/i18n/locales/ko.json create mode 100644 web/src/shared/lib/format-relative-time.test.ts diff --git a/README.md b/README.md index 73811e0e7..fa8c58417 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ with the Skill's source and the problem it solves, or submit a PR by following t expand. - **Pluggable Storage** — Local filesystem for development, S3 / MinIO for production. Swap via config. -- **Internationalization** — Multi-language support with i18next. +- **Internationalization** — Multi-language support with i18next: English, Simplified Chinese, Russian, and Korean. ## Quick Start diff --git a/README_zh.md b/README_zh.md index 326933b00..cf1305862 100644 --- a/README_zh.md +++ b/README_zh.md @@ -64,7 +64,7 @@ Skill,欢迎分享给 SkillHub 社区,与大家一起丰富开放、实用 - **API 令牌管理** — 为 CLI 和程序化访问生成作用域令牌,采用基于前缀的安全哈希。 - **CLI 优先** — 原生 REST API,加上对现有 ClawHub 风格注册中心客户端的兼容层。原生 CLI API 是主要支持路径,协议兼容性持续扩展中。 - **可插拔存储** — 开发环境使用本地文件系统,生产环境使用 S3 / MinIO。通过配置切换。 -- **国际化** — 使用 i18next 支持多语言。 +- **国际化** — 使用 i18next 支持英语、简体中文、俄语和韩语。 ## 快速开始 diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 929b508ed..9f0190bc6 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -265,9 +265,9 @@ export function Layout() {

{t('footer.project')}

diff --git a/web/src/app/router.tsx b/web/src/app/router.tsx index d3b216493..9219ef9d0 100644 --- a/web/src/app/router.tsx +++ b/web/src/app/router.tsx @@ -1,4 +1,5 @@ import { lazy, Suspense, type ComponentType } from 'react' +import { useTranslation } from 'react-i18next' import { createRouter, createRoute, createRootRoute, redirect } from '@tanstack/react-router' import { Layout } from './layout' import { getCurrentUser, type GithubSyncRepositoryQuery, type GithubSyncRunQuery } from '@/api/client' @@ -23,9 +24,10 @@ captureInvitationFragment() export { ORIGINAL_URL_SEARCH } function RouteLoadingFallback() { + const { t } = useTranslation() return (
- Loading... + {t('common.loading')}
) } diff --git a/web/src/features/notification/notification-content.test.ts b/web/src/features/notification/notification-content.test.ts index ef3d9a945..7ff569731 100644 --- a/web/src/features/notification/notification-content.test.ts +++ b/web/src/features/notification/notification-content.test.ts @@ -34,6 +34,46 @@ describe('resolveNotificationDisplay', () => { expect(display.description).toContain('1.0.0') }) + it.each([ + ['REVIEW_SUBMITTED', '스킬 검토 제출'], + ['REVIEW_APPROVED', '스킬 검토 승인'], + ['REVIEW_REJECTED', '스킬 검토 반려'], + ['PROMOTION_SUBMITTED', '스킬 승격 제출'], + ['PROMOTION_APPROVED', '스킬 승격 승인'], + ['PROMOTION_REJECTED', '스킬 승격 반려'], + ['REPORT_SUBMITTED', '스킬 신고 접수'], + ['REPORT_RESOLVED', '스킬 신고 처리 완료'], + ['SKILL_PUBLISHED', '스킬 게시 완료'], + ['SUBSCRIPTION_NEW_VERSION', '구독한 스킬 업데이트'], + ['SUBSCRIPTION_VERSION_YANKED', '구독한 스킬 버전 배포 철회'], + ])('localizes %s in Korean', (eventType, title) => { + const display = resolveNotificationDisplay({ + id: 1, category: 'REVIEW', eventType, title: 'Server title', status: 'UNREAD', + createdAt: '2026-03-20T00:00:00Z', + bodyJson: JSON.stringify({ skillName: 'Calendar', version: '1.0.0' }), + }, 'ko-KR') + expect(display.title).toBe(title) + expect(display.description).toContain('Calendar') + expect(display.description).toMatch(/[가-힣]/) + if (!eventType.startsWith('REPORT_')) expect(display.description).toContain('(1.0.0)') + }) + + it.each([undefined, 'invalid JSON', '{}', 'null'])('keeps a localized title with an absent or malformed body (%s)', (bodyJson) => { + expect(resolveNotificationDisplay({ + id: 1, category: 'REVIEW', eventType: 'REVIEW_APPROVED', title: 'Server title', + status: 'UNREAD', createdAt: '2026-03-20T00:00:00Z', bodyJson, + }, 'ko')).toEqual({ title: '스킬 검토 승인', description: '' }) + }) + + it('omits version parentheses when a notification has no version', () => { + const display = resolveNotificationDisplay({ + id: 1, category: 'REVIEW', eventType: 'REVIEW_APPROVED', title: 'Server title', + status: 'UNREAD', createdAt: '2026-03-20T00:00:00Z', + bodyJson: JSON.stringify({ skillName: 'Calendar' }), + }, 'ko') + expect(display.description).toBe('Calendar의 검토가 승인되었습니다.') + }) + it('falls back to the backend title when the event type is unsupported', () => { const display = resolveNotificationDisplay({ category: 'PUBLISH', diff --git a/web/src/features/notification/notification-content.ts b/web/src/features/notification/notification-content.ts index e034e0867..6a2c09191 100644 --- a/web/src/features/notification/notification-content.ts +++ b/web/src/features/notification/notification-content.ts @@ -1,3 +1,4 @@ +import i18n from '@/i18n/config' import type { NotificationItem } from '@/api/types' export type NotificationDisplay = { @@ -22,77 +23,29 @@ function parseBody(bodyJson?: string): NotificationBody { } } -function isChinese(language: string) { - return language.toLowerCase().startsWith('zh') -} +const localizedEvents = new Set([ + 'REVIEW_SUBMITTED', 'REVIEW_APPROVED', 'REVIEW_REJECTED', + 'PROMOTION_SUBMITTED', 'PROMOTION_APPROVED', 'PROMOTION_REJECTED', + 'REPORT_SUBMITTED', 'REPORT_RESOLVED', 'SKILL_PUBLISHED', + 'SUBSCRIPTION_NEW_VERSION', 'SUBSCRIPTION_VERSION_YANKED', +]) export function resolveNotificationDisplay(item: NotificationItem, language: string): NotificationDisplay { - const zh = isChinese(language) + const zh = language.toLowerCase().startsWith('zh') + const t = i18n.getFixedT(language) const body = parseBody(item.bodyJson) const skillName = body.skillName ?? '' const version = body.version ?? '' const versionSuffix = version ? (zh ? `(${version})` : ` (${version})`) : '' - switch (item.eventType) { - case 'REVIEW_SUBMITTED': - return { - title: zh ? '技能审核提交' : 'Review submitted', - description: skillName ? (zh ? `${skillName}${versionSuffix} 已提交审核。` : `${skillName}${versionSuffix} was submitted for review.`) : '', - } - case 'REVIEW_APPROVED': - return { - title: zh ? '技能审核通过' : 'Review approved', - description: skillName ? (zh ? `${skillName}${versionSuffix} 已审核通过。` : `${skillName}${versionSuffix} was approved.`) : '', - } - case 'REVIEW_REJECTED': - return { - title: zh ? '技能审核驳回' : 'Review rejected', - description: skillName ? (zh ? `${skillName}${versionSuffix} 审核未通过。` : `${skillName}${versionSuffix} was rejected.`) : '', - } - case 'PROMOTION_SUBMITTED': - return { - title: zh ? '技能推广提交' : 'Promotion submitted', - description: skillName ? (zh ? `${skillName}${versionSuffix} 已提交推广。` : `${skillName}${versionSuffix} was submitted for promotion.`) : '', - } - case 'PROMOTION_APPROVED': - return { - title: zh ? '技能推广通过' : 'Promotion approved', - description: skillName ? (zh ? `${skillName}${versionSuffix} 推广已通过。` : `${skillName}${versionSuffix} promotion was approved.`) : '', - } - case 'PROMOTION_REJECTED': - return { - title: zh ? '技能推广驳回' : 'Promotion rejected', - description: skillName ? (zh ? `${skillName}${versionSuffix} 推广未通过。` : `${skillName}${versionSuffix} promotion was rejected.`) : '', - } - case 'REPORT_SUBMITTED': - return { - title: zh ? '技能举报提交' : 'Report submitted', - description: skillName ? (zh ? `${skillName} 收到新的举报。` : `${skillName} received a new report.`) : '', - } - case 'REPORT_RESOLVED': - return { - title: zh ? '技能举报已处理' : 'Report resolved', - description: skillName ? (zh ? `${skillName} 的举报已处理。` : `${skillName} report has been resolved.`) : '', - } - case 'SKILL_PUBLISHED': - return { - title: zh ? '技能发布成功' : 'Skill published', - description: skillName ? (zh ? `${skillName}${versionSuffix} 已发布。` : `${skillName}${versionSuffix} was published.`) : '', - } - case 'SUBSCRIPTION_NEW_VERSION': - return { - title: zh ? '订阅技能更新' : 'Subscribed skill updated', - description: skillName ? (zh ? `${skillName}${versionSuffix} 发布了新版本。` : `${skillName}${versionSuffix} published a new version.`) : '', - } - case 'SUBSCRIPTION_VERSION_YANKED': - return { - title: zh ? '订阅技能版本撤回' : 'Subscribed skill version yanked', - description: skillName ? (zh ? `${skillName}${versionSuffix} 版本已撤回。` : `${skillName}${versionSuffix} version was yanked.`) : '', - } - default: - return { - title: item.title, - description: '', - } + if (!localizedEvents.has(item.eventType)) { + return { title: item.title, description: '' } + } + + return { + title: t(`notificationEvents.${item.eventType}.title`), + description: skillName + ? t(`notificationEvents.${item.eventType}.description`, { skillName, versionSuffix }) + : '', } } diff --git a/web/src/features/skill/skill-avatar-manager.test.tsx b/web/src/features/skill/skill-avatar-manager.test.tsx index f383f435b..085a15302 100644 --- a/web/src/features/skill/skill-avatar-manager.test.tsx +++ b/web/src/features/skill/skill-avatar-manager.test.tsx @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' const api = vi.hoisted(() => ({ status: vi.fn(), delete: vi.fn(), submit: vi.fn(), retry: vi.fn(), appeal: vi.fn() })) vi.mock('@/api/client', () => ({ skillAvatarApi: api, buildApiUrl: (url: string) => url })) -vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key }) })) +vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, options?: { time?: string }) => options?.time ? `${key}: ${options.time}` : key, i18n: { language: 'ko-KR', resolvedLanguage: 'ko' } }) })) import { SkillAvatarManager } from './skill-avatar-manager' describe('SkillAvatarManager appeal', () => { @@ -26,13 +26,14 @@ describe('SkillAvatarManager appeal', () => { }) it('disables paid retry during cooldown and explains when automatic retries stop', async () => { + const nextRetry = new Date(Date.now() + 3600000) api.status.mockResolvedValue({ avatarUrl: null, candidateId: 7, candidateStatus: 'PENDING', machineStatus: 'ERROR', reviewStatus: 'NOT_REQUIRED', candidateUrl: null, reviewReason: null, - machineNextRetryAt: new Date(Date.now() + 3600000).toISOString(), automaticRetryExhausted: true }) + machineNextRetryAt: nextRetry.toISOString(), automaticRetryExhausted: true }) const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) render() - expect(await screen.findByText('skillAvatar.retryAfter')).toBeTruthy() + expect(await screen.findByText(`skillAvatar.retryAfter: ${nextRetry.toLocaleString('ko')}`)).toBeTruthy() expect(screen.getByText('skillAvatar.automaticRetryStopped')).toBeTruthy() expect(screen.getByRole('button', { name: 'skillAvatar.retry' }).hasAttribute('disabled')).toBe(true) }) diff --git a/web/src/features/skill/skill-avatar-manager.tsx b/web/src/features/skill/skill-avatar-manager.tsx index c492b8b31..fb22aa1c0 100644 --- a/web/src/features/skill/skill-avatar-manager.tsx +++ b/web/src/features/skill/skill-avatar-manager.tsx @@ -5,7 +5,7 @@ import { buildApiUrl, skillAvatarApi } from '@/api/client' import { Button } from '@/shared/ui/button' export function SkillAvatarManager({ namespace, slug, disabled = false, previewOnly = false }: { namespace: string; slug: string; disabled?: boolean; previewOnly?: boolean }) { - const { t } = useTranslation() + const { t, i18n } = useTranslation() const queryClient = useQueryClient() const key = ['skill-avatar', namespace, slug] const status = useQuery({ queryKey: key, queryFn: () => skillAvatarApi.status(namespace, slug), enabled: !!namespace && !!slug, staleTime: 0 }) @@ -41,7 +41,7 @@ export function SkillAvatarManager({ namespace, slug, disabled = false, previewO {candidate?.candidateStatus &&

{t('skillAvatar.status')}: {t(`skillAvatar.states.${candidate.candidateStatus}`)}

} {candidate?.reviewReason &&

{candidate.reviewReason}

} - {candidate?.machineStatus === 'ERROR' && retryCoolingDown &&

{t('skillAvatar.retryAfter', { time: new Date(candidate.machineNextRetryAt!).toLocaleString() })}

} + {candidate?.machineStatus === 'ERROR' && retryCoolingDown &&

{t('skillAvatar.retryAfter', { time: new Date(candidate.machineNextRetryAt!).toLocaleString(i18n.resolvedLanguage || i18n.language) })}

} {candidate?.machineStatus === 'ERROR' && candidate.automaticRetryExhausted &&

{t('skillAvatar.automaticRetryStopped')}

} {!previewOnly &&
{candidate?.candidateStatus === 'PENDING' && candidate.machineStatus === 'BLOCKED' diff --git a/web/src/features/skill/skill-card.test.ts b/web/src/features/skill/skill-card.test.ts index b4847b209..46d0b08ac 100644 --- a/web/src/features/skill/skill-card.test.ts +++ b/web/src/features/skill/skill-card.test.ts @@ -12,7 +12,8 @@ vi.mock('@/features/auth/use-auth', () => ({ }), })) -vi.mock('react-i18next', () => ({ +vi.mock('react-i18next', async (importOriginal) => ({ + ...await importOriginal(), useTranslation: () => ({ t: (key: string) => key, i18n: { language: 'en' }, diff --git a/web/src/features/social/skill-reviews.test.tsx b/web/src/features/social/skill-reviews.test.tsx index c150213a1..401655f1a 100644 --- a/web/src/features/social/skill-reviews.test.tsx +++ b/web/src/features/social/skill-reviews.test.tsx @@ -3,6 +3,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import en from '@/i18n/locales/en.json' +import ko from '@/i18n/locales/ko.json' import ru from '@/i18n/locales/ru.json' import zh from '@/i18n/locales/zh.json' @@ -161,7 +162,7 @@ describe('skill reviews', () => { }) it('keeps review interaction copy in every supported locale', () => { - for (const locale of [en, zh, ru]) { + for (const locale of [en, zh, ru, ko]) { expect(locale.skillReviews.ratingDisplay).toBeTruthy() expect(locale.skillReviews.ratingOption).toBeTruthy() expect(locale.skillReviews.reviewTextLabel).toBeTruthy() diff --git a/web/src/i18n/config.test.ts b/web/src/i18n/config.test.ts index dd76b7941..8d9164225 100644 --- a/web/src/i18n/config.test.ts +++ b/web/src/i18n/config.test.ts @@ -29,6 +29,10 @@ vi.mock('./locales/zh.json', () => ({ default: { greeting: '你好' }, })) +vi.mock('./locales/ko.json', () => ({ + default: { greeting: '안녕하세요' }, +})) + vi.mock('./locales/ru.json', () => ({ default: { greeting: 'Привет' }, })) @@ -58,9 +62,10 @@ describe('i18n config', () => { expect(initOptions.detection.caches).toEqual(['localStorage']) }) - it('registers english, russian and chinese resource bundles', () => { + it('registers english, korean, russian and chinese resource bundles', () => { const initOptions = initMock.mock.calls[0][0] expect(initOptions.resources).toHaveProperty('en') + expect(initOptions.resources.ko.translation).toEqual({ greeting: '안녕하세요' }) expect(initOptions.resources).toHaveProperty('ru') expect(initOptions.resources).toHaveProperty('zh') expect(initOptions.resources.en).toHaveProperty('translation') diff --git a/web/src/i18n/config.ts b/web/src/i18n/config.ts index e1cee718c..217f8b6b1 100644 --- a/web/src/i18n/config.ts +++ b/web/src/i18n/config.ts @@ -2,6 +2,7 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' import LanguageDetector from 'i18next-browser-languagedetector' import en from './locales/en.json' +import ko from './locales/ko.json' import ru from './locales/ru.json' import zh from './locales/zh.json' @@ -16,6 +17,7 @@ i18n .init({ resources: { en: { translation: en }, + ko: { translation: ko }, ru: { translation: ru }, zh: { translation: zh }, }, diff --git a/web/src/i18n/github-deep-discovery-drawer-locale.test.ts b/web/src/i18n/github-deep-discovery-drawer-locale.test.ts index 21129a0f9..73adcc22a 100644 --- a/web/src/i18n/github-deep-discovery-drawer-locale.test.ts +++ b/web/src/i18n/github-deep-discovery-drawer-locale.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import en from './locales/en.json' +import ko from './locales/ko.json' import ru from './locales/ru.json' import zh from './locales/zh.json' @@ -10,7 +11,7 @@ const dispositionCodes = [ describe('deep discovery drawer disposition', () => { it('describes every repository detail result in each locale', () => { - for (const locale of [zh, en, ru]) { + for (const locale of [zh, en, ru, ko]) { expect(locale.deepDiscovery.processingStatus).toBeTruthy() for (const code of dispositionCodes) { expect(locale.deepDiscovery.reason[code]).toBeTruthy() diff --git a/web/src/i18n/github-sync-locale.test.ts b/web/src/i18n/github-sync-locale.test.ts index a524c2d3f..d161df647 100644 --- a/web/src/i18n/github-sync-locale.test.ts +++ b/web/src/i18n/github-sync-locale.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import en from './locales/en.json' +import ko from './locales/ko.json' import ru from './locales/ru.json' import zh from './locales/zh.json' @@ -8,6 +9,7 @@ describe('GitHub sync status locales', () => { const expected = Object.keys(zh.githubSync.codes).sort() expect(Object.keys(en.githubSync.codes).sort()).toEqual(expected) expect(Object.keys(ru.githubSync.codes).sort()).toEqual(expected) + expect(Object.keys(ko.githubSync.codes).sort()).toEqual(expected) }) it('describes known acquisition and archive failures for administrators', () => { @@ -20,6 +22,7 @@ describe('GitHub sync status locales', () => { expect(zh.githubSync.codes[code]).not.toBe(code) expect(en.githubSync.codes[code]).toBeTruthy() expect(ru.githubSync.codes[code]).toBeTruthy() + expect(ko.githubSync.codes[code]).toBeTruthy() } }) }) diff --git a/web/src/i18n/ko-locale.test.ts b/web/src/i18n/ko-locale.test.ts new file mode 100644 index 000000000..9cb016d33 --- /dev/null +++ b/web/src/i18n/ko-locale.test.ts @@ -0,0 +1,51 @@ +import { createInstance } from 'i18next' +import { describe, expect, it } from 'vitest' +import en from './locales/en.json' +import ko from './locales/ko.json' + +function flatten(value: unknown, prefix = ''): Record { + if (typeof value === 'string') return { [prefix]: value } + return Object.fromEntries(Object.entries(value as Record).flatMap(([key, child]) => + Object.entries(flatten(child, prefix ? `${prefix}.${key}` : key)), + )) +} + +const pluralSuffix = /_(zero|one|two|few|many|other)$/ +const english = flatten(en) +const korean = flatten(ko) +const normalizedKeys = (keys: string[]) => [...new Set(keys.map((key) => key.replace(pluralSuffix, '_plural')))].sort() +const placeholders = (text: string) => [...text.matchAll(/\{\{[^}]+\}\}/g)].map(([match]) => match).sort() +const tags = (text: string) => [...text.matchAll(/<\/?[\w]+\s*\/?\s*>/g)].map(([match]) => match).sort() + +describe('Korean locale', () => { + it('covers every English key, allowing language-specific plural categories', () => { + expect(normalizedKeys(Object.keys(korean))).toEqual(normalizedKeys(Object.keys(english))) + }) + + it('preserves interpolation variables and markup without empty translations', () => { + for (const [key, value] of Object.entries(korean)) { + const reference = english[key] ?? english[key.replace(pluralSuffix, '_other')] + expect(reference, key).toBeDefined() + expect(placeholders(value), key).toEqual(placeholders(reference)) + expect(tags(value), key).toEqual(tags(reference)) + // The existing pagination suffix is deliberately empty in English. + if (reference.trim()) expect(value.trim(), key).not.toBe('') + } + }) + + it('keeps executable installation commands and registry placeholders intact', () => { + expect(ko.landing.quickStart.cli.command).toBe(en.landing.quickStart.cli.command) + expect(ko.landing.quickStart.human.command).toBe(en.landing.quickStart.human.command) + expect(ko.landing.quickStart.human.commandTemplate).toBe(en.landing.quickStart.human.commandTemplate) + }) + + it('uses Korean plural rules for zero, one, and multiple reviews without English fallback', async () => { + const instance = createInstance() + await instance.init({ lng: 'ko-KR', fallbackLng: 'en', resources: { en: { translation: en }, ko: { translation: ko } } }) + for (const count of [0, 1, 2, 21, 100]) { + const result = instance.t('skillReviews.count', { count, returnDetails: true }) + expect(result.res, String(count)).toBe(`리뷰 ${count}개`) + expect(result.usedLng).toBe('ko') + } + }) +}) diff --git a/web/src/i18n/landing-quick-start-locale.test.ts b/web/src/i18n/landing-quick-start-locale.test.ts index 0dedacf55..0ac447d75 100644 --- a/web/src/i18n/landing-quick-start-locale.test.ts +++ b/web/src/i18n/landing-quick-start-locale.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import skillGuideTemplate from '../docs/skill.md.template?raw' import en from './locales/en.json' +import ko from './locales/ko.json' import ru from './locales/ru.json' import zh from './locales/zh.json' @@ -25,6 +26,7 @@ describe('landing quick start locales', () => { zh.skillDetail.installForAgent.prompt, en.skillDetail.installForAgent.prompt, ru.skillDetail.installForAgent.prompt, + ko.skillDetail.installForAgent.prompt, ]) { expect(prompt).toContain('{{guideUrl}}') expect(prompt).toContain('{{skill}}') diff --git a/web/src/i18n/language-detection.test.ts b/web/src/i18n/language-detection.test.ts new file mode 100644 index 000000000..776506a1f --- /dev/null +++ b/web/src/i18n/language-detection.test.ts @@ -0,0 +1,54 @@ +// @vitest-environment jsdom +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +async function initialize() { + const { default: i18n } = await import('./config') + if (!i18n.isInitialized) await new Promise((resolve) => i18n.on('initialized', () => resolve())) + return i18n +} + +describe('browser language detection with real i18next', () => { + beforeEach(() => { + vi.resetModules() + localStorage.clear() + vi.spyOn(navigator, 'languages', 'get').mockReturnValue(['ko-KR']) + vi.spyOn(navigator, 'language', 'get').mockReturnValue('ko-KR') + }) + + afterEach(() => { + vi.restoreAllMocks() + localStorage.clear() + }) + + it('resolves a Korean browser locale to the Korean resource', async () => { + const i18n = await initialize() + expect(i18n.resolvedLanguage).toBe('ko') + expect(i18n.t('nav.home')).toBe('스킬 센터') + }) + + it('respects an explicit English preference over the Korean browser', async () => { + localStorage.setItem('i18nextLng', 'en') + const i18n = await initialize() + expect(i18n.resolvedLanguage).toBe('en') + }) + + it('persists a Korean selection and restores it on a fresh initialization', async () => { + vi.spyOn(navigator, 'languages', 'get').mockReturnValue(['en-US']) + vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-US') + const i18n = await initialize() + expect(i18n.resolvedLanguage).toBe('en') + await i18n.changeLanguage('ko') + expect(localStorage.getItem('i18nextLng')).toBe('ko') + vi.resetModules() + const restored = await initialize() + expect(restored.resolvedLanguage).toBe('ko') + expect(restored.t('nav.home')).toBe('스킬 센터') + }) + + it('keeps English fallback for an unsupported language', async () => { + localStorage.setItem('i18nextLng', 'fr-FR') + const i18n = await initialize() + expect(i18n.resolvedLanguage).toBe('en') + expect(i18n.t('nav.home')).toBe('Skill Center') + }) +}) diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 5e6697977..20cd47539 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -6,6 +6,7 @@ "evidence": "Evidence" }, "common": { + "loading": "Loading...", "expand": "Expand details", "collapse": "Collapse details" }, @@ -379,6 +380,7 @@ "enterpriseContinue": "Log in with work account", "switchToPersonal": "Use personal account instead", "agreementPrefix": "By logging in, you agree to our", + "agreement": "By logging in, you agree to our Terms of Service and Privacy Policy", "terms": "Terms of Service", "and": "and", "privacy": "Privacy Policy" @@ -2208,6 +2210,9 @@ } }, "footer": { + "license": "License", + "contributing": "Contributing", + "changelog": "Changelog", "product": "Product", "marketplace": "Skill marketplace", "publish": "Publish skill", @@ -3240,5 +3245,57 @@ "scanPASSED": "Security scan passed.", "scanFAILED": "Security scan did not pass. Current visibility is unchanged. Withdraw and retry or upload a fixed version.", "pending": "Pending visibility request: {{visibility}}. Complete or withdraw it in My review progress." + }, + "relativeTime": { + "justNow": "just now", + "minutes": "{{count}}m", + "hours": "{{count}}h", + "days": "{{count}}d" + }, + "notificationEvents": { + "REVIEW_SUBMITTED": { + "title": "Review submitted", + "description": "{{skillName}}{{versionSuffix}} was submitted for review." + }, + "REVIEW_APPROVED": { + "title": "Review approved", + "description": "{{skillName}}{{versionSuffix}} was approved." + }, + "REVIEW_REJECTED": { + "title": "Review rejected", + "description": "{{skillName}}{{versionSuffix}} was rejected." + }, + "PROMOTION_SUBMITTED": { + "title": "Promotion submitted", + "description": "{{skillName}}{{versionSuffix}} was submitted for promotion." + }, + "PROMOTION_APPROVED": { + "title": "Promotion approved", + "description": "{{skillName}}{{versionSuffix}} promotion was approved." + }, + "PROMOTION_REJECTED": { + "title": "Promotion rejected", + "description": "{{skillName}}{{versionSuffix}} promotion was rejected." + }, + "REPORT_SUBMITTED": { + "title": "Report submitted", + "description": "{{skillName}} received a new report." + }, + "REPORT_RESOLVED": { + "title": "Report resolved", + "description": "{{skillName}} report has been resolved." + }, + "SKILL_PUBLISHED": { + "title": "Skill published", + "description": "{{skillName}}{{versionSuffix}} was published." + }, + "SUBSCRIPTION_NEW_VERSION": { + "title": "Subscribed skill updated", + "description": "{{skillName}}{{versionSuffix}} published a new version." + }, + "SUBSCRIPTION_VERSION_YANKED": { + "title": "Subscribed skill version yanked", + "description": "{{skillName}}{{versionSuffix}} version was yanked." + } } } diff --git a/web/src/i18n/locales/ko-legal.json b/web/src/i18n/locales/ko-legal.json new file mode 100644 index 000000000..8fd4fbd38 --- /dev/null +++ b/web/src/i18n/locales/ko-legal.json @@ -0,0 +1,156 @@ +{ + "terms": { + "eyebrow": "법률 안내", + "title": "이용약관", + "summary": "본 약관은 SkillHub의 탐색, 게시, 검토, 다운로드, 계정 관리 및 관련 API 서비스에 대한 접근과 이용에 적용됩니다. 서비스를 이용하면 본 약관에 동의한 것으로 봅니다.", + "lastUpdated": "최종 업데이트: 2026년 3월 14일", + "note": "조직에서 SkillHub를 비공개로 배포하여 운영하는 경우, 본 기본 약관에 더하여 내부 규칙, 정보 보안 요건 또는 허용되는 이용 정책을 적용할 수 있습니다.", + "sections": [ + { + "title": "1. 동의 및 적용 범위", + "paragraphs": [ + "SkillHub에 접근하거나 가입하거나 이용하면 본 이용약관, 관련 개인정보처리방침 및 현재 인스턴스의 운영 규칙에 동의한 것으로 봅니다.", + "팀, 회사 또는 기타 조직을 대신하여 SkillHub를 이용하는 경우, 해당 조직을 대신하여 본 약관에 동의할 권한이 있음을 진술합니다." + ] + }, + { + "title": "2. 계정 및 접근 보안", + "paragraphs": [], + "bullets": [ + "정확한 최신 계정 정보를 제공하고 자격 증명, OAuth 세션, API 토큰을 보호해야 합니다.", + "게시, 검토, 다운로드, 토큰 생성, 네임스페이스 관리를 포함하여 계정을 통해 수행되는 활동에 대한 책임은 본인에게 있습니다.", + "무단 접근, 자격 증명 유출 또는 기타 보안 사고를 알게 되면 인스턴스 관리자나 운영자에게 즉시 알려야 합니다." + ] + }, + { + "title": "3. 스킬, 네임스페이스 및 사용자 콘텐츠", + "paragraphs": [ + "업로드하거나 제출한 콘텐츠에 대한 권리는 본인에게 유지됩니다. 다만 서비스 운영에 필요한 범위에서 해당 콘텐츠를 호스팅, 저장, 복사, 처리, 표시, 배포할 수 있는 비독점적이고 전 세계적이며 재허가 가능한 라이선스를 SkillHub에 부여합니다.", + "제출하는 스킬 패키지, README 파일, 설명, 스크린샷, 검토 의견 및 기타 자료에 대한 책임은 본인에게 있으며, 이를 제공할 권리가 있음을 진술합니다." + ], + "bullets": [ + "악성 소프트웨어, 불법 자료, 권리를 침해하는 자료, 기만적 자료 또는 사용자를 오도할 목적으로 작성된 콘텐츠를 업로드해서는 안 됩니다.", + "타인을 사칭하거나 권한 없이 네임스페이스를 차지하거나 검토, 심사 또는 접근 제어를 우회해서는 안 됩니다.", + "스킬을 다운로드하거나 재배포할 때는 해당 스킬의 라이선스 조건, 제3자 의존성의 라이선스 및 관련 법률도 준수해야 합니다." + ] + }, + { + "title": "4. 검토, 거버넌스 및 조치", + "paragraphs": [ + "SkillHub는 서비스의 보안, 규정 준수 및 품질을 보호하기 위해 콘텐츠를 검토하고 승인, 반려, 숨김, 제거, 버전 배포 철회, 접근 제한 또는 계정 정지 등의 조치를 취할 수 있습니다.", + "악용, 권리 침해, 보안 위험, 불법 행위 또는 기타 위반이 의심되는 경우 플랫폼 또는 인스턴스 관리자는 로그를 보존하고 적절한 조치를 취할 수 있습니다." + ] + }, + { + "title": "5. 다운로드, API 및 허용되는 이용", + "paragraphs": [], + "bullets": [ + "서비스의 안정성을 방해하거나 인증, 요청 제한, 보안 제어 또는 권한 경계를 우회해서는 안 됩니다.", + "파괴적인 방식으로 SkillHub를 대상으로 스크래핑, 부하 테스트, 스캔 또는 자동화를 수행하거나 서비스를 이용하여 바이러스, 트로이 목마 또는 기타 악성 페이로드를 배포해서는 안 됩니다.", + "SkillHub를 통해 다운로드한 스킬은 해당 게시자가 제공하며 각 스킬의 라이선스와 위험 안내에 따라 사용해야 합니다. 호환성, 보안 및 규정 준수를 평가할 책임은 본인에게 있습니다." + ] + }, + { + "title": "6. 오픈소스 구성 요소, 지식재산 및 브랜드", + "paragraphs": [ + "SkillHub에는 Apache License 2.0 또는 기타 제3자 라이선스가 적용되는 오픈소스 구성 요소가 포함될 수 있으며, 해당 구성 요소에는 각 라이선스 조건이 계속 적용됩니다.", + "별도로 명시하지 않는 한 SkillHub의 상표, 브랜드 자산, 인터페이스 디자인 및 사이트 콘텐츠는 각 소유자의 재산이며 서비스 이용으로 이전되지 않습니다." + ] + }, + { + "title": "7. 가용성 및 보증 부인", + "paragraphs": [ + "검색, 다운로드, 검토 워크플로, 로그인 방식 및 API 기능을 포함한 서비스의 일부를 언제든지 수정, 업데이트, 제한 또는 중단할 수 있으며, 지속적인 이용 가능성을 보장하지 않습니다.", + "법률이 허용하는 최대 범위에서 SkillHub는 상품성, 특정 목적에의 적합성, 비침해성, 중단 없는 이용 가능성 또는 정확성에 대한 보증을 포함하여 명시적이거나 묵시적인 보증 없이 ‘있는 그대로’ 및 ‘이용 가능한 상태로’ 제공됩니다." + ] + }, + { + "title": "8. 책임 제한", + "paragraphs": [ + "법률이 허용하는 최대 범위에서 SkillHub와 운영자는 간접적, 부수적, 특별, 결과적 또는 징벌적 손해나 이익, 데이터 또는 영업권의 손실에 대해 책임을 지지 않습니다.", + "서비스 이용, 제출한 콘텐츠 또는 본 약관 위반으로 청구가 발생하는 경우 법률이 허용하는 범위에서 책임을 부담하고 문제 해결에 협력하는 데 동의합니다." + ] + }, + { + "title": "9. 종료, 변경 및 문의", + "paragraphs": [ + "언제든지 SkillHub 이용을 중단할 수 있습니다. 약관을 위반하거나 보안 위험을 초래하거나 법률상 필요한 경우 접근을 일시 중지하거나 종료할 수 있습니다.", + "본 약관은 수시로 업데이트될 수 있습니다. 업데이트 후에도 계속 이용하면 개정된 약관에 동의한 것으로 봅니다. 문의하려면 현재 인스턴스에서 제공하는 문서, 커뮤니티 또는 관리자 채널을 이용하세요." + ] + } + ] + }, + "privacy": { + "eyebrow": "법률 안내", + "title": "개인정보처리방침", + "summary": "본 방침은 SkillHub가 스킬 레지스트리, 게시, 검토, 다운로드, 계정 및 관련 API 서비스를 제공할 때 정보를 수집, 이용, 공유 및 보호하는 방식을 설명합니다.", + "lastUpdated": "최종 업데이트: 2026년 3월 14일", + "note": "비공개로 배포된 SkillHub 인스턴스를 이용하는 경우, 해당 운영자는 자체 내부 정책에 따라 데이터를 처리할 수 있으며 해당 환경에서 처리하는 데이터에 대해 독립적으로 행동할 수 있습니다.", + "sections": [ + { + "title": "1. 적용 범위", + "paragraphs": [ + "본 방침은 SkillHub 웹사이트, 웹 콘솔, 공개 스킬 페이지, 로그인 절차, 기기 인증 절차 및 관련 API와 서비스에 적용됩니다.", + "스킬 탐색, 버전 게시, 검토 참여, 토큰 생성 또는 콘텐츠 다운로드 시 본인과 관련된 정보를 처리하는 방식을 설명합니다." + ] + }, + { + "title": "2. 수집하는 정보", + "paragraphs": [], + "bullets": [ + "사용자 이름, 이메일, 아바타, OAuth 제공자 식별자, 플랫폼 역할, 네임스페이스 소속 등의 계정 및 신원 정보.", + "스킬 패키지, README 파일, 릴리스 노트, 네임스페이스 프로필, 평점, 즐겨찾기, 검토 의견 등 제출한 콘텐츠.", + "IP 주소, 브라우저 또는 기기 정보, 요청 로그, 다운로드 활동, 로그인 이벤트, API 토큰 메타데이터, 오류 로그, 감사 로그 등의 이용 및 보안 정보." + ] + }, + { + "title": "3. 정보 이용 방식", + "paragraphs": [], + "bullets": [ + "로그인, 세션 관리, 접근 제어, 기기 인증, 계정 보안 및 기본 지원 제공.", + "공개 스킬 페이지 표시와 검색, 다운로드, 평점, 즐겨찾기, 네임스페이스 협업 및 거버넌스 워크플로 제공.", + "검토, 요청 제한, 악용 방지, 디버깅, 성능 분석 및 서비스 개선 수행.", + "필요한 경우 보안, 정책 변경 또는 서비스 가용성과 관련된 중요 안내 전달." + ] + }, + { + "title": "4. 공개 정보 및 공유", + "paragraphs": [ + "공개로 게시한 스킬, 릴리스 노트, 네임스페이스 이름, 공개 평점 및 일부 프로필 정보는 다른 사용자나 방문자에게 표시될 수 있습니다.", + "개인정보를 판매하지 않습니다. 호스팅, 인증, 모니터링 또는 규정 준수 지원을 제공하는 데 필요하거나 법률을 준수하고 서비스와 사용자를 보호하기 위해 정보를 공유할 수 있습니다.", + "비공개 배포의 경우 인스턴스 운영자는 자체 내부 거버넌스 및 규정 준수 요건에 따라 데이터에 접근하거나 처리하거나 보관할 수 있습니다." + ] + }, + { + "title": "5. 데이터 보관", + "paragraphs": [ + "서비스 운영에 필요한 기간 동안 계정 정보, 스킬 메타데이터, 검토 기록, 보안 로그를 보관하며 법률, 감사 또는 규정 준수 목적에 필요한 경우 더 오래 보관할 수 있습니다.", + "폐기된 API 토큰은 더 이상 작동하지 않지만 관련 보안 및 감사 기록은 남을 수 있습니다. 스킬을 삭제하거나 숨긴 후에도 백업이나 로그에 남은 사본은 합리적인 기간 동안 유지될 수 있습니다." + ] + }, + { + "title": "6. 선택권 및 권리", + "paragraphs": [], + "bullets": [ + "계정 정보를 업데이트하고 비밀번호를 변경하며 API 토큰을 폐기하거나 다시 만들 수 있습니다.", + "인스턴스 관리자에게 본인 관련 정보의 내보내기, 정정 또는 삭제를 요청할 수 있습니다. 다만 보안, 감사 또는 규정 준수를 위해 일부 기록을 보관해야 할 수 있습니다.", + "비공개로 배포된 인스턴스에서는 일반적으로 해당 인스턴스 운영자에게 먼저 개인정보 관련 요청을 해야 합니다." + ] + }, + { + "title": "7. 보안 조치", + "paragraphs": [ + "접근 제어, 인증, 감사, 토큰 관리, 전송 보안 등의 합리적인 기술적·조직적 보호 조치를 사용합니다.", + "어떤 인터넷 서비스도 절대적인 보안을 보장할 수 없습니다. 자격 증명을 보호하고 의심스러운 접근, 유출 또는 오용을 발견하면 인스턴스 운영자에게 즉시 알려야 합니다." + ] + }, + { + "title": "8. 방침 업데이트 및 문의", + "paragraphs": [ + "제품, 법률 요건 또는 운영 방식이 변경되면 본 방침을 업데이트할 수 있습니다. 최신 버전은 서비스에 게시되며 업데이트 날짜로 구분됩니다.", + "문의하려면 현재 SkillHub 인스턴스에서 제공하는 문서, 커뮤니티, 지원 채널 또는 관리자 연락처를 이용하세요." + ] + } + ] + } +} diff --git a/web/src/i18n/locales/ko.json b/web/src/i18n/locales/ko.json new file mode 100644 index 000000000..b4ce56797 --- /dev/null +++ b/web/src/i18n/locales/ko.json @@ -0,0 +1,3582 @@ +{ + "compliance": { + "title": "규정 준수 선언", + "mappingCount": "{{count}}개 항목", + "unknownStandard": "알 수 없는 표준", + "evidence": "근거" + }, + "common": { + "loading": "불러오는 중...", + "expand": "상세 정보 펼치기", + "collapse": "상세 정보 접기" + }, + "nav": { + "landing": "홈", + "home": "스킬 센터", + "search": "검색", + "marketplace": "스킬 마켓플레이스", + "githubSkills": "오픈소스 스킬", + "skillDetail": "스킬 상세 정보", + "explore": "스킬 탐색", + "dashboard": "대시보드", + "mySkills": "내 스킬", + "mySuites": "내 스킬 모음", + "publish": "게시", + "suites": "스킬 모음", + "login": "로그인" + }, + "githubCatalog": { + "title": "오픈소스 스킬", + "sourceLabel": "공개 GitHub 저장소", + "description": "SkillHub에서 검토하고 게시한 공개 GitHub 저장소의 스킬을 탐색하세요. 각 스킬 페이지에서 출처, 경로, 고정된 커밋, 라이선스 정보를 확인할 수 있습니다.", + "explore": "오픈소스 스킬 둘러보기", + "allSkills": "모든 스킬 보기", + "publishedCount": "게시된 GitHub 스킬", + "countNote": "@github에서 게시되어 공개된 스킬만 집계합니다.", + "resultsTitle": "스킬 탐색", + "resultsDescription": "@github 카탈로그에서 이름이나 설명으로 스킬을 검색하세요.", + "resultCount": "검색 결과 {{count}}개", + "searchPlaceholder": "오픈소스 스킬 검색", + "search": "검색", + "sortLabel": "정렬 기준", + "viewSkill": "스킬 보기", + "noSummary": "아직 설명이 없습니다", + "newest": "최근 업데이트순", + "mostDownloaded": "다운로드 많은 순", + "loading": "스킬을 불러오는 중…", + "loadFailed": "스킬을 불러오지 못했습니다", + "empty": "검색 조건에 맞는 게시된 GitHub 스킬이 없습니다." + }, + "landing": { + "hero": { + "title": "AI 스킬을 발견하고 공유하세요", + "subtitle": "커뮤니티의 스킬로 강력한 AI 에이전트를 만드세요", + "searchPlaceholder": "스킬 검색...", + "exploreSkills": "스킬 탐색", + "publishSkill": "스킬 게시" + }, + "features": { + "secure": { + "title": "보안과 개인정보 보호", + "description": "AI 워크플로를 위한 기업 수준의 보안" + }, + "community": { + "title": "커뮤니티 중심", + "description": "전 세계 개발자의 스킬을 발견하고 공유하세요" + }, + "integration": { + "title": "간편한 연동", + "description": "기존 도구와 원활하게 연동하세요" + }, + "versionControl": { + "title": "버전 관리", + "description": "체계적인 버전 관리와 릴리스 워크플로로 스킬 패키지의 추적 가능성과 신뢰성을 유지합니다." + }, + "cli": { + "title": "CLI 도구", + "description": "명령줄에서 스킬 패키지를 게시하고 설치하고 관리할 수 있습니다." + }, + "governance": { + "title": "검토 거버넌스", + "description": "내장된 검토 절차와 권한 제어로 기업 스킬의 품질을 관리합니다." + } + }, + "stats": { + "skills": "카탈로그", + "downloads": "다운로드", + "teams": "팀" + }, + "whySkillHub": { + "title": "SkillHub를 선택하는 이유", + "subtitle": "기업 팀을 위해 설계된 비공개 에이전트 스킬 플랫폼" + }, + "badge": "기업용 스킬 레지스트리", + "tagline": "게시, 탐색, 관리", + "taglineHighlight": " 에이전트 스킬", + "description": "팀이 안전하고 효율적으로 스킬을 공유하고 협업할 수 있는 자체 호스팅 비공개 스킬 레지스트리", + "publishSkill": "스킬 게시", + "statsSkills": "스킬", + "statsDownloads": "다운로드", + "statsTeams": "팀", + "whyTitle": "선택하는 이유", + "whyDescription": "게시, 탐색, 거버넌스를 모두 지원하는 기업용 스킬 관리 플랫폼", + "featuresList": { + "privateDeploy": { + "title": "비공개 배포", + "description": "자체 호스팅으로 데이터 주권을 확보하세요. 자체 인프라에 한 번의 클릭으로 배포하고 방화벽 내부에서 안전하게 운영할 수 있습니다." + }, + "versionControl": { + "title": "버전 관리", + "description": "시맨틱 버전 관리, 사용자 지정 태그(beta, stable), 최신 버전 자동 추적을 지원합니다." + }, + "smartSearch": { + "title": "스마트 검색", + "description": "네임스페이스, 다운로드 수, 평점, 시간 등 다양한 필터를 지원하는 전문 검색입니다." + }, + "teamwork": { + "title": "팀 협업", + "description": "네임스페이스 관리, 역할 기반 접근 제어(소유자/관리자/멤버), 게시 정책 설정을 지원합니다." + }, + "governance": { + "title": "검토 및 거버넌스", + "description": "팀 단위 검토, 플랫폼 단위 승인, 전체 감사 추적으로 규정 준수 요건을 충족합니다." + }, + "cliFirst": { + "title": "CLI 우선", + "description": "기본 REST API를 제공하며 기존 ClawHub CLI 도구와 호환되므로 클라이언트를 변경할 필요가 없습니다." + } + }, + "ctaTitle": "시작할 준비가 되셨나요?", + "ctaDescription": "명령어 하나로 로컬 개발 환경을 시작하고 전체 스킬 관리 워크플로를 경험하세요", + "ctaButton": "지금 시작하기", + "footerLicense": "© 2026 SkillHub. Apache License 2.0.", + "footerDocs": "문서", + "footerGithub": "GitHub", + "footerCommunity": "커뮤니티", + "quickStart": { + "title": "빠른 시작", + "subtitle": "빠른 시작", + "description": "작업 방식을 선택한 다음 설정 안내를 복사하여 진행하세요", + "tip": "💡 팁: 스킬 상세 페이지에서 환경변수가 포함된 설치 명령어를 한 번에 복사할 수 있습니다", + "tabs": { + "agent": "에이전트로 시작", + "human": "직접 시작", + "cli": "CLI" + }, + "cli": { + "description": "SkillHub CLI를 로컬에 설치하면 skillhub install 명령어로 스킬을 설치할 수 있습니다.", + "command": "npm i -g @astron-team/skillhub" + }, + "agent": { + "description": "에이전트에 프롬프트를 보내 SkillHub 레지스트리를 설정하세요", + "command": "https://www.example.com/registry/skill.md를 사용하여 SkillHub에 연결하세요", + "commandTemplate": "{{url}}을 사용하여 SkillHub에 연결하세요" + }, + "human": { + "description": "SkillHub CLI로 스킬을 검색하고 설치하세요", + "command": "npx @astron-team/skillhub@latest search --registry https://www.example.com", + "commandTemplate": "npx @astron-team/skillhub@latest search --registry {{url}}" + }, + "steps": { + "configureEnv": { + "title": "1. 환경변수 설정", + "description": "ClawHub CLI가 SkillHub에 연결되도록 설정하세요" + }, + "installSkills": { + "title": "2. 스킬 설치", + "description": "필요한 스킬을 검색하고 설치하세요", + "code": "# 스킬 검색\nclawhub search \n\n# 스킬 설치\nclawhub install " + }, + "publishSkills": { + "title": "3. 스킬 게시", + "description": "팀에 스킬을 공유하세요", + "code": "# 스킬 게시\nclawhub publish\n\n# 또는 웹 화면에서\n# \"스킬 게시\"를 클릭하세요" + } + } + }, + "experience": { + "heroBadge": "에이전트 스킬 관리 플랫폼", + "heroTitle": "팀의 전문 지식을 에이전트가 활용할 수 있는 스킬로 만드세요", + "heroDescription": "개인의 AI 활용 노하우를 조직 전체에서 재사용하고 관리하고 신뢰할 수 있도록 하세요.", + "browseAll": "전체 보기", + "marketplace": "마켓플레이스", + "latest": "최신", + "demoSkills": { + "weather": "전 세계 도시의 실시간 날씨 조회", + "gitHelper": "Git 워크플로 지원", + "diagramMaker": "순서도 및 아키텍처 다이어그램 생성", + "codeReviewer": "자동 코드 리뷰", + "caption": "화면 미리보기 · 예제 스킬 4개" + }, + "highlights": { + "selfHosted": "자체 호스팅 레지스트리", + "governance": "검토 및 거버넌스", + "multiClient": "에이전트, CLI, 웹 접근", + "traceable": "추적 가능한 버전" + }, + "capabilities": { + "eyebrow": "주요 기능", + "title": "등록부터 배포까지 스킬의 전체 수명 주기를 관리하세요", + "description": "패키지 저장소를 넘어 게시, 검토, 배포, 거버넌스를 하나의 플랫폼에서 제공합니다." + }, + "enterprise": { + "title": "팀과 기업을 위한 설계", + "description": "자체 호스팅, 접근 제어, 검토 워크플로, 감사 기능을 하나의 레지스트리에서 제공합니다.", + "clientLayer": "클라이언트", + "serviceLayer": "서비스", + "storageLayer": "스토리지", + "networkBoundary": "조직 내부에 배포하여 레지스트리 데이터를 직접 관리하세요", + "deployment": { + "title": "자체 호스팅 배포", + "description": "Docker Compose 또는 제공된 Kubernetes 매니페스트로 자체 인프라에서 SkillHub를 실행하고, 로컬 또는 S3 호환 오브젝트 스토리지를 사용하세요." + }, + "security": { + "title": "검증 및 검토", + "description": "패키지 검증, 보안 검사, 검토 워크플로를 통해 게시 전에 스킬을 평가할 수 있습니다." + }, + "rbac": { + "title": "계층별 RBAC", + "description": "플랫폼 역할과 네임스페이스 역할로 조직 전체 관리와 팀 소유 권한을 구분합니다." + }, + "access": { + "title": "유연한 접근 방식", + "description": "브라우저에서는 OAuth2와 로컬 계정으로 로그인하고, 프로그램에서는 API 토큰으로 접근할 수 있습니다." + }, + "audit": { + "title": "감사 추적", + "description": "감사 기록을 통해 주요 게시, 다운로드, 삭제, 권한 부여 작업을 추적할 수 있습니다." + }, + "openSource": { + "title": "오픈소스", + "description": "Apache-2.0 라이선스로 공개된 코드를 검토하거나 자체 호스팅하고 커뮤니티에 기여할 수 있습니다." + } + }, + "cta": { + "title": "팀의 스킬 레지스트리를 구축하세요", + "description": "소스와 배포 안내를 확인한 후 자체 환경에서 SkillHub를 실행하세요.", + "deploy": "소스 보기", + "docs": "문서 읽기" + }, + "quickStart": { + "title": "접근 방식 선택", + "description": "에이전트, CLI 또는 웹 화면에서 동일한 레지스트리를 사용할 수 있습니다.", + "copy": "복사", + "copied": "복사됨", + "copyErrorTitle": "복사 실패", + "modes": { + "agent": { + "title": "에이전트 연동", + "description": "레지스트리를 한 번 설정하고 필요할 때 스킬을 탐색하세요" + }, + "cli": { + "title": "CLI", + "description": "스킬 검색, 설치, 게시" + }, + "web": { + "title": "웹 화면", + "description": "화면에서 스킬을 탐색하고 읽고 다운로드하세요" + } + }, + "agent": { + "tablist": "에이전트 연동 데모", + "registryTab": "레지스트리 설정", + "discoveryTab": "자동 탐색", + "live": "에이전트 실행 중", + "instruction": "{{url}}을 읽고 안내에 따라 SkillHub 스킬 레지스트리를 설정하세요", + "instructionLead": "이 안내를 복사하여 에이전트에 보내세요", + "instructionPrefix": "다음 문서를 확인하세요:", + "instructionSuffix": "안내에 따라 SkillHub 스킬 레지스트리를 설정하세요.", + "copyInstruction": "안내 복사", + "copyErrorDescription": "레지스트리 설정 안내를 직접 선택하여 복사하세요.", + "steps": { + "read": "안내 읽기", + "configure": "레지스트리 설정", + "discover": "필요할 때 스킬 탐색" + }, + "prompt": "오늘 허페이 날씨는 어떤가요?", + "discovery": { + "intentTitle": "작업 파악", + "intentDescription": "실시간 날씨와 이동 안내가 필요합니다", + "searchTitle": "SkillHub 레지스트리 검색", + "searchDescription": "@global/weather · v1.3.0 일치", + "readTitle": "스킬 사용 안내 읽기", + "readDescription": "입력 및 호출 규약 확인" + }, + "answerLead": "오늘 허페이는 흐리고 기온은 26°C입니다", + "answerTail": ". 오후에 잠깐 소나기가 내릴 수 있으니 우산을 챙기세요." + }, + "cli": { + "copyErrorDescription": "CLI 명령어를 직접 선택하여 복사하세요.", + "skillsFound": "예제 결과:", + "forecastSummary": "7일간의 일기 예보", + "installed": "@global/weather를 ./skills/global/weather에 설치했습니다", + "connected": "연결됨", + "docs": "CLI 문서" + }, + "web": { + "marketplace": "스킬 마켓플레이스", + "publicSkills": "공개 스킬", + "search": "스킬 검색", + "status": "공개 · 사용 가능", + "summary": "실시간 날씨, 일기 예보, 이동 정보", + "download": "ZIP 다운로드", + "readTitle": "사용 전에 읽어보세요", + "readDescription": "전체 패키지를 다운로드하기 전에 SKILL.md에서 목적, 입력값, 실행 안내를 확인하세요.", + "version": "버전", + "files": "파일", + "format": "형식", + "browse": "탐색 및 검색", + "preview": "콘텐츠 미리보기", + "versions": "파일 및 버전", + "favorites": "즐겨찾기 및 구독" + } + } + } + }, + "home": { + "subtitle": "스킬 레지스트리", + "description": "개발자를 위한 효율적인 스킬 관리, 배포, 협업 플랫폼", + "browseSkills": "스킬 둘러보기", + "publishSkill": "스킬 게시", + "popularTitle": "인기 다운로드", + "popularDescription": "커뮤니티에서 가장 인기 있는 스킬", + "latestTitle": "최신 릴리스", + "latestDescription": "새로 게시된 스킬", + "viewAll": "전체 보기 →", + "quickStart": { + "title": "빠른 시작", + "subtitle": "빠른 시작", + "description": "몇 가지 간단한 단계로 SkillHub를 시작하세요", + "tip": "💡 팁: 스킬 상세 페이지에서 환경변수가 포함된 설치 명령어를 한 번에 복사할 수 있습니다", + "steps": { + "configureEnv": { + "title": "1. 환경변수 설정", + "description": "ClawHub CLI가 SkillHub에 연결되도록 설정하세요" + }, + "installSkills": { + "title": "2. 스킬 설치", + "description": "필요한 스킬을 검색하고 설치하세요", + "code": "# 스킬 검색\nclawhub search \n\n# 스킬 설치\nclawhub install " + }, + "publishSkills": { + "title": "3. 스킬 게시", + "description": "팀에 스킬을 공유하세요", + "code": "# 스킬 게시\nclawhub publish\n\n# 또는 웹 화면에서\n# \"스킬 게시\"를 클릭하세요" + } + } + } + }, + "search": { + "title": "스킬 검색", + "placeholder": "스킬 검색...", + "filters": { + "label": "필터:" + }, + "sort": { + "label": "정렬:", + "relevance": "관련도순", + "downloads": "다운로드", + "stars": "즐겨찾기", + "newest": "최신순" + }, + "noResults": "검색 결과가 없습니다", + "noResultsFor": "\"{{q}}\"에 해당하는 스킬을 찾지 못했습니다", + "filterStarred": "즐겨찾기만 보기", + "noStarredResults": "즐겨찾기한 스킬이 없습니다", + "noStarredResultsFor": "\"{{q}}\"에 해당하는 즐겨찾기 스킬이 없습니다", + "noStarredSkills": "아직 즐겨찾기한 스킬이 없습니다", + "namespaceFilter": "@{{namespace}}", + "enterKeyword": "검색어를 입력하세요", + "results": "스킬 {{count}}개를 찾았습니다", + "resultCount": "검색 결과 <1>{{count}}개", + "loadingMore": "검색 결과를 업데이트하는 중..." + }, + "searchBar": { + "placeholder": "스킬 검색...", + "button": "검색", + "clear": "검색어 지우기" + }, + "login": { + "title": "SkillHub 로그인", + "subtitle": "하나의 계정으로 더 나은 스킬 협업을 시작하세요", + "eyebrow": "다시 오신 것을 환영합니다", + "tabEnterprise": "조직 로그인", + "tabPersonal": "개인 로그인", + "loginMode": "로그인 방식", + "enterpriseTitle": "기업 SSO", + "enterpriseHint": "관리자가 업무용 로그인 항목을 설정하면 기업 계정으로 로그인할 수 있습니다.", + "enterpriseConfigHint": "업무용 로그인 항목은 관리자가 활성화합니다.", + "noEnterpriseMethods": "아직 업무용 계정 로그인을 사용할 수 없습니다. 개인 계정을 사용하거나 관리자에게 문의하세요.", + "personalTitle": "비밀번호 로그인", + "personalHint": "SkillHub 사용자 이름과 비밀번호를 사용하세요.", + "otherMethods": "또는 다음으로 계속하기", + "orgLoginHint": "통합 로그인이 설정된 조직용", + "externalIdentityMethods": "외부 인증 제공자", + "externalIdentityHint": "Feishu, DingTalk, GitHub, GitLab 등의 로그인 항목은 서버에서 제공합니다. 계정 소유권은 서버의 계정 연결과 조직 소속에 따라 결정됩니다.", + "tabPassword": "비밀번호", + "tabOAuth": "OAuth", + "username": "사용자 이름", + "password": "비밀번호", + "usernamePlaceholder": "사용자 이름 입력", + "passwordPlaceholder": "비밀번호 입력", + "usernameRequired": "사용자 이름을 입력하세요", + "passwordRequired": "비밀번호를 입력하세요", + "showPassword": "비밀번호 표시", + "hidePassword": "비밀번호 숨기기", + "submitting": "로그인하는 중...", + "submit": "로그인", + "forgotPassword": "비밀번호를 잊으셨나요?", + "noAccount": "계정이 없으신가요?", + "register": "회원가입", + "oauthHint": "OAuth 인증 후 이 사이트로 자동으로 돌아옵니다.", + "enterpriseSsoTitle": "기업 SSO", + "enterpriseSsoHint": "이 배포 환경에서는 호환 계층이 활성화되어 있습니다. 브라우저에 {{name}} 세션이 있다면 SkillHub 세션을 바로 생성해 볼 수 있습니다.", + "enterpriseSsoAutoHint": "이 배포 환경에서는 {{name}} 자동 감지가 활성화되어 있습니다. 실패하면 일반 로그인 방식을 사용할 수 있습니다.", + "enterpriseSsoAction": "{{name}}으로 시도", + "enterpriseSsoSubmitting": "{{name}}으로 시도하는 중...", + "enterpriseContinue": "업무용 계정으로 로그인", + "switchToPersonal": "개인 계정 사용", + "agreementPrefix": "로그인하면 다음 정책에 동의하게 됩니다:", + "agreement": "로그인하면 이용약관 및 개인정보처리방침에 동의하게 됩니다.", + "terms": "이용약관", + "and": "및", + "privacy": "개인정보처리방침" + }, + "register": { + "title": "계정 만들기", + "subtitle": "로컬 계정을 만들거나 OAuth로 바로 로그인하세요.", + "eyebrow": "개인 계정 만들기", + "tabLocal": "로컬 계정", + "tabOAuth": "OAuth", + "username": "사용자 이름", + "email": "이메일", + "password": "비밀번호", + "usernamePlaceholder": "영문자, 숫자, 밑줄로 구성된 3~64자", + "emailPlaceholder": "이메일 입력", + "emailRequired": "이메일을 입력하세요", + "passwordPlaceholder": "3가지 문자 유형을 포함한 8자 이상", + "submitting": "가입하는 중...", + "submit": "가입 및 로그인", + "hasAccount": "이미 계정이 있으신가요?", + "login": "로그인으로 돌아가기", + "oauthHint": "로컬 비밀번호 없이 기존 OAuth 계정으로 바로 로그인하세요.", + "usernameRequired": "사용자 이름을 입력하세요", + "usernameInvalid": "영문자, 숫자, 밑줄만 사용할 수 있습니다(3~64자)", + "usernameExists": "이미 사용 중인 사용자 이름입니다", + "passwordRequired": "비밀번호를 입력하세요", + "passwordTooShort": "비밀번호는 8자 이상이어야 합니다", + "passwordTooWeak": "비밀번호에는 3가지 이상의 문자 유형(영문 대문자, 소문자, 숫자, 특수문자)이 포함되어야 합니다", + "emailInvalid": "이메일 형식이 올바르지 않습니다", + "emailExists": "이미 사용 중인 이메일입니다" + }, + "resetPassword": { + "title": "비밀번호 재설정", + "subtitle": "이메일, 인증 코드, 새 비밀번호를 입력하세요.", + "email": "이메일", + "emailPlaceholder": "이메일 입력", + "emailRequired": "이메일을 입력하세요", + "emailInvalid": "올바른 이메일 주소를 입력하세요", + "code": "인증 코드", + "codePlaceholder": "6자리 인증 코드 입력", + "sendCode": "인증 코드 보내기", + "sendingCode": "보내는 중...", + "codeSentMessage": "대상 계정이 조건을 충족하는 경우 인증 코드를 보냈습니다.", + "codeRequired": "인증 코드를 입력하세요", + "newPassword": "새 비밀번호", + "newPasswordPlaceholder": "새 비밀번호 입력", + "newPasswordRequired": "새 비밀번호를 입력하세요", + "confirmPassword": "비밀번호 확인", + "confirmPasswordPlaceholder": "새 비밀번호 다시 입력", + "passwordMismatch": "두 비밀번호가 일치하지 않습니다", + "submit": "비밀번호 재설정", + "submitting": "재설정하는 중...", + "successMessage": "비밀번호가 재설정되었습니다. 새 비밀번호로 로그인하세요.", + "genericError": "비밀번호를 재설정하지 못했습니다", + "backToLogin": "로그인으로 돌아가기" + }, + "device": { + "title": "기기 인증", + "subtitle": "기기에 표시된 8자리 사용자 코드를 입력하세요", + "codeLabel": "사용자 코드", + "codeHint": "형식: XXXX-XXXX(붙여넣기 가능)", + "incompleteCode": "8자리 사용자 코드를 모두 입력하세요", + "success": "기기가 인증되었습니다!", + "defaultError": "인증에 실패했습니다. 사용자 코드를 확인하세요", + "submitting": "인증하는 중...", + "submit": "기기 인증", + "notice": "인증 후 해당 기기에서 계정에 접근할 수 있습니다" + }, + "cliAuth": { + "validating": "검증하는 중...", + "pleaseWait": "잠시 기다려 주세요", + "creatingToken": "토큰을 생성하는 중...", + "almostThere": "거의 완료되었습니다", + "success": "인증 완료", + "redirecting": "CLI로 이동하는 중...", + "fallbackInstructions": "브라우저가 자동으로 이동하지 않으면 아래 토큰을 복사하세요:", + "error": "인증 실패", + "notAuthenticated": "로그인되어 있지 않습니다", + "invalidRedirectUri": "리디렉션 URI가 올바르지 않습니다", + "missingState": "보안 매개변수가 누락되었습니다", + "windowsUrlBug": "Windows의 알려진 문제로 브라우저에서 불완전한 URL이 열렸습니다. CLI 출력에서 'Opening browser:'로 시작하는 줄의 전체 URL을 복사하여 브라우저 주소 표시줄에 붙여넣으세요.", + "tokenCreationFailed": "토큰 생성 실패", + "loginRequired": "CLI 접근을 인증하려면 먼저 로그인하세요", + "goToLogin": "로그인으로 이동" + }, + "dashboard": { + "title": "대시보드", + "subtitle": "스킬, 계정, 환경설정을 관리하세요", + "overview": "개요", + "backToDashboard": "대시보드로 돌아가기", + "navigation": "대시보드 탐색", + "userInfo": "계정 정보", + "userInfoDesc": "기본 계정 정보 및 플랫폼 역할", + "loginVia": "{{provider}}로 로그인됨", + "platformRoles": "플랫폼 역할", + "starsAndRatings": "즐겨찾기 및 평점", + "viewStars": "내 즐겨찾기 보기", + "subscriptions": "구독", + "viewSubscriptions": "내 구독 보기", + "mySkillsTitle": "내 스킬", + "openMySkills": "내 스킬 보기", + "mySkillsPreviewDescription": "최근 스킬 5개를 표시합니다. 스킬을 열거나 내 스킬에서 전체 목록을 확인하세요.", + "mySkillsPreviewEmpty": "아직 게시한 스킬이 없습니다", + "credentials": "자격 증명", + "openTokens": "API 토큰 보기", + "governanceTitle": "검토 및 거버넌스", + "viewGovernance": "거버넌스 센터 열기", + "viewPromotions": "승격 보기", + "reportsTitle": "신고 관리", + "viewReports": "스킬 신고 보기", + "previewMore": "...", + "previewMoreLabel": "전체 보기", + "userId": "사용자 ID" + }, + "sidebar": { + "account": "계정", + "profile": "프로필", + "security": "보안", + "accounts": "계정 병합", + "notifications": "알림", + "skillsAndData": "스킬 및 데이터", + "mySkills": "내 스킬", + "mySuites": "내 스킬 모음", + "publish": "게시", + "namespaces": "네임스페이스", + "stars": "내 즐겨찾기", + "subscriptions": "구독", + "tokens": "API 토큰", + "reviewProgress": "검토 진행 상황", + "admin": "관리", + "governance": "거버넌스", + "reports": "신고", + "githubSync": "스킬 동기화", + "deepDiscovery": "심층 탐색" + }, + "deepDiscovery": { + "title": "심층 탐색", + "subtitle": "공개 GitHub 스킬 저장소 공식 탐색", + "configure": "설정", + "configDescription": "플랫폼 관리자가 활성화 여부, 예산, 제외 대상을 관리합니다. 저장된 진행 상황에서 작업이 계속됩니다.", + "enabled": "심층 탐색 활성화", + "enabledHelp": "끄면 새로운 공식 탐색과 전용 후보 처리가 일시 중지됩니다. 등록된 소스는 계속 업데이트됩니다.", + "budget": "탐색 요청 예산", + "budgets": { + "ECONOMY": "절약", + "BALANCED": "균형", + "ACCELERATED": "가속" + }, + "budgetHelp": { + "ECONOMY": "낮은 리소스 사용량", + "BALANCED": "속도와 리소스의 균형", + "ACCELERATED": "빠른 탐색, 높은 리소스 사용량" + }, + "budgetScope": "채널을 제거하지 않고 공식 검색과 소유자 열거 속도를 조절합니다. 파일 다운로드는 별도이며 할당량과 용량 제한은 계속 적용됩니다.", + "exclusions": "제외할 저장소 또는 소유자", + "exclusionsHelp": "일치하는 미등록 후보만 일시 중지되며 등록된 소스에는 영향을 주지 않습니다.", + "exclusionKind": "제외 유형", + "exclusionRepository": "저장소", + "exclusionOwner": "소유자 또는 조직", + "exclusionValue": "GitHub 식별 경로", + "removeExclusion": "{{value}} 제거", + "noExclusions": "제외 대상 없음", + "add": "추가", + "save": "설정 저장", + "cancel": "취소", + "configSaved": "탐색 설정을 저장했습니다", + "configSaveFailed": "설정을 저장하지 못했습니다", + "configLoadFailed": "설정을 불러오지 못했습니다", + "configConflict": "다른 관리자가 이 설정을 변경했습니다. 최신 값을 다시 불러와 확인한 후 저장하세요.", + "reloadConfiguration": "최신 설정 다시 불러오기", + "paused": "일시 중지됨", + "pausedTitle": "탐색이 일시 중지되었으며 후보와 진행 상황은 보존됩니다", + "pausedHelp": "다시 활성화하면 공식 탐색이 재개됩니다. 등록된 저장소는 자체 일정에 따라 계속 처리됩니다.", + "worker": "수집기", + "period": "결과 기간", + "lastDays": "최근 {{count}}일", + "recentLabel": "선택한 기간", + "currentLabel": "기간과 무관한 현재 대기 작업", + "newCandidates": "새 후보 저장소", + "validRepositories": "처음 검증된 저장소", + "newSkills": "새로 수집된 스킬", + "updatedVersions": "업데이트된 콘텐츠 버전", + "pendingChecks": "확인할 저장소", + "pendingReviews": "검토 대기 버전", + "notPublished": "수집은 게시를 의미하지 않습니다", + "incompleteHistory": "일부 과거 지표를 사용할 수 없습니다. 완전한 근거는 {{date}}부터 제공됩니다.", + "observedAt": "관측 시각", + "progressTab": "탐색 진행 상황", + "repositoriesTab": "발견된 저장소", + "channels": "탐색 채널", + "channelHeading": "채널", + "completedShards": "완료된 작업 단위", + "pendingShards": "대기 중인 작업 단위", + "gapShards": "누락된 탐색 범위", + "lastProgress": "마지막 처리 진행", + "statusHeading": "상태", + "noPercentage": "작업 단위는 GitHub 저장소 총수를 뜻하지 않으며, 탐색 범위의 비율은 제공되지 않습니다.", + "waiting": "대기 및 복구", + "affectedWork": "영향을 받는 작업", + "continuingWork": "계속 진행 중", + "retryAt": "가장 빠른 재시도", + "autoResume": "조건이 충족되면 자동으로 재개됩니다.", + "viewCapacity": "용량 설정 보기", + "viewCredentials": "자격 증명 보기", + "quotaObservations": "최근 할당량 관측", + "quotaHelp": "할당량은 리소스와 주체별로 관측하며 여러 토큰의 값을 합산하지 않습니다. 알 수 없음은 0을 의미하지 않습니다.", + "resource": "요청 리소스", + "remaining": "마지막 관측 잔여량", + "quotaTruncated": "관측 결과가 잘렸습니다. 전체 자격 증명 풀을 표시한 것이 아닙니다.", + "recentRuns": "최근 실행", + "noRecentRuns": "관련 실행 기록 없음", + "viewRun": "실행 상세 보기", + "searchLabel": "저장소 또는 소유자 검색", + "searchPlaceholder": "소유자/저장소 검색", + "search": "검색", + "filterStatus": "상태별 필터", + "filterChannel": "채널별 필터", + "filterPaused": "일시 중지 상태별 필터", + "allStatuses": "모든 상태", + "allChannels": "모든 채널", + "allPaused": "모든 일시 중지 상태", + "onlyPaused": "일시 중지만", + "onlyActive": "일시 중지 아님", + "refresh": "결과 새로고침", + "newResultsHint": "새 저장소 결과가 있습니다. 새로고침하여 확인하세요. 현재 목록은 유지됩니다.", + "repositoryHeading": "저장소", + "actionHeading": "작업", + "firstDiscovered": "최초 발견", + "lastDiscovered": "마지막 일치", + "resultCount": "저장소 {{count}}개", + "asOf": "삽입 기준 시점", + "asOfHelp": "페이지는 첫 조회의 삽입 기준 시점을 유지합니다. 기존 기록의 상태는 계속 변경될 수 있습니다.", + "paginationLimit": "이 API에서는 처음 {{count}}개 결과만 탐색할 수 있습니다. 총수는 변하지 않으므로 검색어나 필터로 범위를 좁히세요.", + "detailPaginationLimit": "이 상세 화면에서는 처음 {{count}}개 기록만 탐색할 수 있습니다. 이후 기록은 아직 여기에서 볼 수 없습니다.", + "registeredSource": "등록된 소스", + "viewDetail": "상세 보기", + "repositoryDetail": "저장소 상세 정보", + "detailHelp": "탐색 근거, 최근 확인 결과, 다음 단계를 검토하세요.", + "provenance": "탐색 근거", + "seedRepository": "검증된 시드 저장소", + "checkResult": "확인 결과", + "lastChecked": "마지막 확인", + "nextEligible": "다음 처리 가능 시점", + "processingStatus": "처리 상태", + "pathResults": "최근 경로 결과", + "noPaths": "경로 결과 없음", + "noProvenance": "보존된 탐색 근거 없음", + "viewSource": "소스 보기", + "viewReview": "검토 보기", + "viewReviewQueue": "동기화 검토 대기열 열기", + "loading": "불러오는 중…", + "loadFailed": "불러오지 못했습니다", + "empty": "기록 없음", + "state": { + "DISABLED": "꺼짐", + "RUNNING": "실행 중", + "WAITING": "대기 중", + "IDLE": "현재 처리할 작업 없음" + }, + "workerState": { + "AVAILABLE": "호환 워커 연결됨", + "INCOMPATIBLE": "연결된 워커에 필요한 기능이 없음", + "UNREACHABLE": "연결 끊김", + "NEVER_SEEN": "연결된 적 없음" + }, + "status": { + "PENDING": "확인 대기", + "VALIDATING": "확인 중", + "VALID": "검증됨", + "NO_VALID_SKILL": "유효한 스킬 없음", + "VALIDATION_FAILED": "검증 실패", + "UNREACHABLE": "저장소 접근 불가" + }, + "channel": { + "REPOSITORY_METADATA": "저장소 메타데이터", + "README": "README 단서", + "OWNER_EXPANSION": "소유자 범위 확장" + }, + "reason": { + "DISABLED": "현재 일시 중지됨", + "EXCLUDED": "제외됨", + "BUDGET": "작업 예산 대기 중", + "PRIMARY_RATE_LIMIT": "GitHub 기본 요청 제한", + "SECONDARY_COOLDOWN": "GitHub 보조 제한 대기", + "QUOTA_COOLDOWN": "공유 요청 대기", + "CREDENTIAL_UNAVAILABLE": "자격 증명 사용 불가", + "CANDIDATE_CAPACITY": "후보 용량 가득 참", + "REVIEW_BACKPRESSURE": "검토 적체 보호", + "COVERAGE_GAP": "탐색 범위 누락", + "WORKER_UNAVAILABLE": "수집기 연결 대기 중", + "CAPABILITY_UNAVAILABLE": "호환 수집기 없음", + "NOT_CHECKED": "아직 확인하지 않음", + "WAITING_CAPACITY": "용량 확보 대기 중", + "NO_VALID_SKILL": "유효한 스킬 없음", + "VALIDATION_FAILED": "검증 실패", + "SOURCE_UNREADABLE": "현재 저장소를 읽을 수 없습니다", + "CONFLICT": "스킬 충돌 해결 필요", + "REVIEW_PENDING": "검토를 요청했으며 결정을 기다리는 중" + }, + "recovery": { + "DISABLED": "관련 소스 또는 탐색 설정을 복원하면 재개됩니다.", + "EXCLUDED": "제외 설정을 제거하면 저장된 진행 상황에서 재개됩니다.", + "BUDGET": "다음 처리 가능 시점이 지나고 이전 탐색 작업이 종료되면 수집기가 재개됩니다.", + "PRIMARY_RATE_LIMIT": "할당량이 복구되면 작업을 가져올 수 있습니다.", + "SECONDARY_COOLDOWN": "대기 시간이 지나면 작업을 가져올 수 있습니다.", + "QUOTA_COOLDOWN": "공유 대기 시간이 지나면 작업을 가져올 수 있습니다.", + "CREDENTIAL_UNAVAILABLE": "사용 가능한 자격 증명이 생기면 재개됩니다.", + "CANDIDATE_CAPACITY": "후보 처리로 용량이 확보되면 재개됩니다.", + "REVIEW_BACKPRESSURE": "적체량이 복구 기준 아래로 감소하면 재개됩니다.", + "COVERAGE_GAP": "시스템은 누락 범위를 보존하고 대기 간격을 늘려 재시도합니다.", + "WORKER_UNAVAILABLE": "수집기가 다시 연결되면 재개됩니다.", + "CAPABILITY_UNAVAILABLE": "호환 수집기가 다시 연결되면 재개됩니다." + }, + "work": { + "NEW_DISCOVERY": "새 저장소 탐색", + "CANDIDATE_VALIDATION": "후보 검증", + "PACKAGE_ACQUISITION": "스킬 패키지 수집", + "SOURCE_UPDATE": "소스 업데이트" + }, + "quotaState": { + "OBSERVED": "마지막 관측", + "UNKNOWN": "알 수 없음", + "PRIMARY_EXHAUSTED": "기본 할당량 소진", + "SECONDARY_COOLDOWN": "보조 제한 대기", + "CREDENTIAL_UNAVAILABLE": "자격 증명 사용 불가" + }, + "pathOutcome": { + "ACCEPTED": "접수됨", + "UNCHANGED": "변경 없음", + "IGNORED": "무시됨", + "RETRYABLE_FAILURE": "재시도 가능한 실패", + "TERMINAL_FAILURE": "최종 실패", + "NO_CHANGE": "변경 없음" + } + }, + "githubSync": { + "unifiedTitle": "스킬 동기화", + "unifiedSubtitle": "공개 GitHub 스킬의 지속적인 탐색, 수집, 업데이트를 관리합니다", + "collectionConfig": "수집 설정", + "systemSettings": "시스템 설정", + "collectionScope": "이 설정은 플랫폼 @github 소스에 적용되며 팀 소스는 독립적으로 유지됩니다.", + "automaticDiscovery": "자동 탐색", + "automaticDiscoveryHelp": "시스템이 저장된 진행 상황에서 공개 저장소를 찾습니다. 활성화 여부, 예산, 제외 대상을 관리하세요.", + "specifiedRepository": "지정된 저장소", + "specifiedRepositoryHelp": "공개 GitHub 저장소 URL 또는 owner/repo를 입력하세요. 저장하면 시스템이 확인 일정을 등록합니다.", + "repositoryQueued": "저장소를 저장했습니다. 일정에 따라 확인하며 기존 항목은 현재 상태를 유지합니다.", + "noSpecifiedRepositories": "아직 지정된 저장소가 없습니다.", + "latestExecution": "최근 실행", + "viewRepository": "저장소 보기", + "otherAcquisition": "다른 수집 방식", + "otherAcquisitionHelp": "필요에 따라 선택한 계정과 사용자 지정 검색 규칙을 관리하세요.", + "preparation": "초기 설정", + "preparationHelp": "현재 서비스와 설정 상태를 확인하세요. 자격 증명은 선택 사항이며 익명 요청에는 더 낮은 할당량이 적용됩니다.", + "configured": "설정됨", + "notConfigured": "설정되지 않음", + "noEnabledAcquisition": "활성화된 수집 방식 없음", + "credentialConfigured": "활성 시스템 자격 증명을 사용할 수 있습니다", + "anonymousQuota": "자격 증명 없음: 익명 공개 API 할당량 사용 중", + "acquisitionMethod": "수집 방식", + "statusUnknown": "상태를 확인할 수 없음", + "someStatusUnavailable": "일부 상태 요청에 실패했습니다. 나중에 새로고침하거나 관련 설정을 확인하세요.", + "currentWaits": "현재 대기 상태", + "operatingNormally": "현재 수동 조치가 필요한 수집 대기 항목이 없습니다.", + "repositoriesTab": "저장소", + "repositoriesHelp": "모든 탐색 방식의 결과를 저장소 ID별로 묶습니다. 등록은 유효한 스킬 또는 게시를 의미하지 않습니다.", + "registeredRepositories": "등록된 저장소", + "registeredRepositoriesHelp": "플랫폼 소스가 존재하지만 유효한 스킬은 보장되지 않습니다.", + "pendingLeads": "대기 중인 단서", + "pendingLeadsHelp": "검증을 기다리는 통합 저장소입니다.", + "officialRecentResult": "최근 7일 자동 탐색: 새 후보 {{candidates}}개, 수집된 스킬 {{skills}}개", + "partialHistory": "과거 지표가 불완전합니다", + "editAutomaticDiscovery": "자동 탐색 편집", + "discoveryProgress": "탐색 진행 상황", + "repositoryDetail": "저장소 상세 정보", + "repositoryDetailHelp": "각 스킬 경로의 출처, 확인 결과, 처리 결과를 살펴보세요.", + "repositoryStateLabel": "등록 상태", + "discoveryMethod": "탐색 방식", + "searchRepository": "저장소 검색", + "search": "검색", + "all": "전체", + "firstSeen": "최초 확인", + "origins": "탐색 출처", + "sourceActions": "등록된 소스 작업", + "newRepositoryResults": "더 최근의 저장소를 찾았습니다. 새로고침하면 첫 페이지부터 다시 탐색할 수 있습니다.", + "refreshResults": "결과 새로고침", + "repositoryState": { + "REGISTERED": "소스 등록됨", + "PENDING": "검증 대기", + "VALIDATING": "검증 중", + "NO_VALID_SKILL": "유효한 스킬을 찾지 못함", + "VALIDATION_FAILED": "검증 실패", + "UNREACHABLE": "저장소 접근 불가", + "BOUND_WITHOUT_SOURCE": "소스 없이 연결됨" + }, + "sourceStatus": { + "ACTIVE": "소스 활성화됨", + "PAUSED": "소스 일시 중지됨", + "UNREACHABLE": "소스 접근 불가" + }, + "platformControl": "플랫폼 수집", + "compatibleWorker": "플랫폼 작업을 가져올 수 있는 Go 워커", + "manualPauseNotice": "관리자가 플랫폼 수집을 일시 중지했습니다. 접수된 패키지는 검증과 검토를 계속합니다.", + "capacityPauseNotice": "용량 보호로 새 패키지 접수가 일시 중지되었습니다. 기존 임계값에 따라 작업이 재개됩니다.", + "platformControlScope": "@github에만 적용됩니다. 워커 연결, 용량, 자동 탐색 활성화 여부는 별도로 관리됩니다.", + "controlState": { + "RUNNING": "플랫폼 작업을 가져올 수 있음", + "PAUSING": "안전한 체크포인트에서 일시 중지하는 중", + "PAUSED": "플랫폼 수집 일시 중지됨" + }, + "pausePlatform": "플랫폼 수집 일시 중지", + "resumePlatform": "플랫폼 수집 재개", + "pausePlatformHelp": "새로운 @github 작업을 가져오지 않습니다. 진행 중인 작업은 안전한 체크포인트에서 멈춥니다. 접수된 패키지는 계속 처리되며 팀 소스에는 영향을 주지 않습니다.", + "resumePlatformHelp": "저장된 진행 상황에서 @github 작업을 재개합니다. 별도로 비활성화한 방식과 소스는 비활성 상태를 유지합니다.", + "confirmControl": "확인", + "controlPauseRequested": "플랫폼 일시 중지를 요청했습니다. 진행 중인 작업의 중지 확인을 기다리세요.", + "controlResumeRequested": "플랫폼 수집 재개를 요청했습니다.", + "controlConflict": "다른 관리자가 제어 상태를 변경하여 최신 상태로 새로고침했습니다.", + "pendingStops": "진행 중인 작업 {{count}}개의 중지가 아직 확인되지 않았습니다", + "expiredStops": "리스 {{count}}개가 확인 없이 만료되었습니다", + "repositoryFilter": { + "CURATED_REPOSITORY": "지정된 저장소", + "CURATED_OWNER": "지정된 계정", + "SEARCH_RULE": "사용자 지정 검색", + "OFFICIAL_DISCOVERY": "자동 탐색", + "TEAM_CONFIG": "팀 설정", + "ACTIVE": "활성화됨", + "PAUSED": "일시 중지됨", + "UNREACHABLE": "접근 불가", + "REGISTERED": "소스 등록됨", + "PENDING": "검증 대기", + "VALIDATING": "검증 중", + "NO_VALID_SKILL": "유효한 스킬을 찾지 못함", + "VALIDATION_FAILED": "검증 실패", + "BOUND_WITHOUT_SOURCE": "소스 없이 연결됨" + }, + "sourceDetailHelp": "등록된 소스의 스킬 경로와 작업을 확인하세요.", + "enabled": "활성화됨", + "disabled": "비활성화됨", + "resolveConflict": "슬러그 충돌 해결", + "resolvePathMove": "경로 변경 확인", + "confirmPathMoveTitle": "스킬 소스 경로 확인", + "confirmPathMoveHelp": "새 경로 {{path}}를 자동으로 연결하지 못했습니다. 연결된 스킬 ID를 확인하고 기존 경로가 사라졌는지 확인하세요. 콘텐츠가 변경되었으면 동기화 검토로 전달되며, 동일하면 소스 경로만 업데이트됩니다.", + "bindExistingTitle": "기존 스킬에 연결", + "bindExistingHelp": "소스 경로 {{path}}는 스킬 슬러그 {{slug}}를 대상으로 합니다. 이 네임스페이스의 스킬을 확인한 다음 ID와 사유를 입력하세요. 검토용 버전이 생성되면 기술적 소유권이 시스템 계정으로 이전되고 네임스페이스에서 스킬을 관리합니다.", + "targetSkillId": "기존 스킬 ID", + "bindingReason": "사유(10자 이상)", + "confirmBinding": "연결 후 재시도", + "bindingQueued": "연결을 기록했습니다. 패키지를 다시 처리합니다", + "bindingFailed": "연결 실패", + "title": "GitHub 스킬 동기화", + "teamLink": "GitHub 소스", + "teamTitle": "@{{slug}}의 GitHub 소스", + "teamSubtitle": "이 네임스페이스에 공개 저장소를 등록하고 동기화 진행 상황을 확인하세요", + "visibility": "향후 가져올 스킬의 공개 범위", + "namespaceOnly": "네임스페이스 전용", + "public": "공개", + "visibilityHint": "공개 범위 변경은 향후 가져올 스킬에만 적용됩니다. 게시된 스킬은 변경되지 않습니다.", + "repositoryId": "GitHub 저장소 ID", + "unresolved": "식별 대기", + "subtitle": "공개 저장소 소스, 수집 실행, 스킬별 진행 상황", + "overview": "개요", + "settings": "설정", + "settingsHelp": "수집 간격, 용량 보호, 공개 읽기 자격 증명을 관리하세요.", + "viewSettings": "설정 보기", + "actionItems": "확인 필요", + "actionUnresolvedHint": "대기 중인 검토, 차단된 항목, 기타 미해결 동기화 작업을 포함합니다.", + "openReviewQueue": "검토 대기열 열기", + "sourceCountHint": "등록된 공개 GitHub 저장소 소스입니다.", + "viewSources": "소스 보기", + "configureSources": "소스 설정", + "recentRuns": "최근 실행", + "viewAllRuns": "전체 실행", + "viewRun": "실행 보기", + "runsHelp": "탐색 및 저장소 실행을 확인하세요. 수집 완료는 게시를 의미하지 않습니다.", + "discoveryTitle": "소스 및 탐색", + "discoveryIntro": "저장소를 추가하거나 계정을 팔로우하거나 규칙에 따라 공개 저장소를 지속적으로 탐색하세요.", + "discoveryMode": "소스 방식 선택", + "existingEntries": "소스 및 탐색 규칙", + "existingEntriesHint": "상태와 다음 예정 시각을 확인하거나 검색 규칙을 편집하세요.", + "rulePreviewTitle": "규칙 미리보기", + "previewPrompt": "규칙을 입력하고 예상 GitHub 검색 결과를 미리 확인하세요.", + "capacityTitle": "수집 용량", + "platformWatermark": "플랫폼 하한 / 상한", + "namespaceWatermark": "네임스페이스 하한 / 상한", + "candidateBudget": "대기 중인 저장소 단서 한도", + "admissionState": "새 패키지 접수", + "globalUnresolved": "플랫폼 전체의 현재 미해결 항목", + "platformUnresolved": "@github의 현재 미해결 항목", + "platformPendingCandidates": "@github에서 검증 대기 중인 저장소", + "platformAdmissionState": "@github의 새 패키지 접수", + "workerStatusTitle": "수집기 상태", + "workerState": "Go 서비스", + "workerOnline": "온라인", + "workerOffline": "오프라인", + "workerNeverConnected": "연결된 적 없음", + "workerLastSeen": "마지막 확인", + "paused": "일시 중지됨", + "active": "열림", + "unconfigured": "임계값이 설정되지 않음", + "config": "탐색 설정", + "credentials": "시스템 자격 증명", + "credentialHelp": "공개 GitHub API 읽기 전용입니다. 토큰은 암호화되며 저장 후에는 볼 수 없습니다. 자격 증명이 설정되지 않은 경우에만 익명으로 접근합니다. 설정된 자격 증명을 사용할 수 없으면 복구를 기다립니다.", + "credentialLabel": "자격 증명 이름", + "credentialToken": "GitHub 토큰", + "addCredential": "자격 증명 추가", + "replaceCredential": "토큰 교체", + "cancel": "취소", + "noCredentials": "설정된 시스템 자격 증명이 없습니다. 공개 저장소에는 익명으로 접근할 수 있습니다.", + "fingerprint": "토큰 지문", + "keyVersion": "암호화 키 버전", + "keyUnavailable": "키 사용 불가", + "rateLimitedUntil": "요청 제한 종료 시각", + "observedQuota": "마지막 관측 할당량", + "lastUsed": "마지막 사용", + "capacityConfigHelp": "변경 사항은 새 패키지에 즉시 적용됩니다. 하한은 상한보다 작아야 하며 리스당 단서 수는 대기 한도를 초과할 수 없습니다.", + "globalHigh": "플랫폼 상한", + "globalLow": "플랫폼 하한", + "namespaceHigh": "네임스페이스 상한", + "namespaceLow": "네임스페이스 하한", + "candidatePendingLimit": "대기 중인 저장소 단서 한도", + "candidatePerLeaseLimit": "탐색 리스당 단서 수", + "invalidWatermarks": "임계값 또는 예산 값이 올바르지 않습니다", + "save": "설정 저장", + "addRepository": "저장소 추가", + "repositoryHelp": "탐색 및 수집할 공개 저장소를 등록하세요.", + "repositoryFormat": "GitHub 저장소 URL 또는 owner/repository 입력", + "addOwner": "선별 계정 추가", + "ownerHelp": "이 계정의 공개 저장소를 주기적으로 조회합니다.", + "owner": "GitHub 사용자 또는 조직", + "addRule": "검색 규칙 추가", + "editRule": "검색 규칙 편집", + "edit": "편집", + "ruleHelp": "새 규칙은 아래에서 활성화하기 전까지 비활성 상태입니다.", + "ruleTitle": "규칙 이름", + "targetHours": "목표 간격(시간)", + "hoursUnit": "시간", + "selectorKind": "조건 유형", + "selectorPriority": "조건 우선순위", + "selectorValue": "키워드 또는 토픽", + "keyword": "키워드", + "topic": "토픽", + "core": "핵심", + "extension": "확장", + "addSelector": "조건 추가", + "remove": "제거", + "add": "추가", + "saved": "저장됨", + "saveFailed": "저장하지 못했습니다", + "ruleSavedDisabled": "규칙을 비활성 상태로 저장했습니다", + "previewRule": "규칙 미리보기", + "previewWaiting": "수집기가 GitHub 저장소를 검색하고 있습니다.", + "previewQueued": "미리보기가 대기열에 등록되었습니다. 수집기가 작업을 가져올 때까지 기다립니다.", + "previewDeferred": "미리보기가 일시적으로 지연되었습니다. 자동으로 재시도합니다.", + "previewWorkerChecking": "수집기 상태를 확인하는 중…", + "previewWorkerUnknown": "수집기 상태를 확인할 수 없습니다. 미리보기 전에 새로고침하세요.", + "previewWorkerOffline": "수집기가 오프라인이므로 미리보기를 실행할 수 없습니다. 규칙을 비활성 상태로 저장한 후 수집기가 다시 연결되면 미리 볼 수 있습니다.", + "previewMissingResult": "결과 없이 미리보기가 종료되었습니다. 다시 시도하세요.", + "previewStale": "규칙 입력값이 변경되었습니다. 현재 결과를 확인하려면 다시 미리 보세요.", + "previewFailed": "미리보기 실패", + "previewEstimate": "예상 저장소 일치 수", + "previewLimited": "GitHub가 불완전한 결과를 반환했거나 검색 페이지 한도를 초과했습니다. 이 값은 추정치로 참고하세요.", + "previewCaveat": "여러 선택 조건이 동일한 저장소와 일치할 수 있습니다. 메타데이터 검색은 저장소를 놓칠 수 있습니다. 미리보기는 소스를 등록하거나 대기 스킬을 생성하지 않습니다.", + "discoveryCoverageHint": "자동 검색은 저장소 메타데이터를 사용하므로 설정된 조건 밖의 저장소를 놓칠 수 있습니다. 실행 완료가 모든 스킬의 발견을 의미하지는 않습니다. 알려진 누락을 보완하려면 저장소를 직접 지정하세요.", + "ignorePaths": "무시할 소스 경로", + "ignorePathsHelp": "한 줄에 저장소 기준 상대 디렉터리를 하나씩, 최대 64개까지 입력하세요. 후보 SKILL.md 파일만 무시합니다. 기본 제외 규칙은 계속 적용되며 게시된 스킬은 계속 사용할 수 있습니다.", + "currentRule": "현재 규칙", + "entryType": "항목 유형", + "entryName": "항목", + "ruleVersion": "규칙 버전", + "enable": "활성화", + "disable": "비활성화", + "capacityHint": "상한에 도달하면 수집을 일시 중지하고 하한 아래로 내려가면 재개합니다. 수집은 게시를 의미하지 않습니다.", + "scheduleTitle": "수집 목표 간격", + "scheduleHelp": "유형별 목표 간격을 시간 단위로 설정하세요. 저장하면 기존 항목도 업데이트됩니다. 대기열 또는 지연 상태의 작업에는 할당량과 적체 제한이 계속 적용됩니다. 개별 검색 규칙은 사용자 지정 간격을 사용할 수 있습니다.", + "scheduleLegacy": "저장소 확인에는 현재 배포 설정 {{interval}}이 적용됩니다. 관리 화면에서 관리하려면 여기에 간격을 저장하세요.", + "scheduleInvalid": "목표 간격은 1~720 사이의 정수 시간이어야 합니다.", + "repositoryHours": "저장소 업데이트", + "curatedOwnerHours": "선별된 사용자 또는 조직", + "searchRuleHours": "광범위 검색", + "targetHoursDefault": "비워 두면 유형별 목표 간격을 사용합니다.", + "sources": "저장소 소스", + "runs": "실행", + "repository": "저장소", + "sourceState": "상태", + "received": "전달됨, 검증 대기 중", + "processing": "Java에서 처리 중", + "pendingReview": "동기화 검토 대기", + "blocked": "차단됨", + "failed": "실패", + "packageStages": "패키지 단계", + "failureReason": "사유:", + "nextCheck": "다음 확인", + "details": "상세 정보", + "viewSkills": "스킬 보기", + "rescan": "재검사 예약", + "rescanScheduled": "확인 일정을 앞당겼으며 스케줄러 순서에 따라 실행합니다.", + "rescanMerged": "기존 작업에 병합했습니다.", + "rescanRetryWait": "재시도 대기 작업에 병합했습니다. 재시도 시각은 변경되지 않습니다.", + "rescanPaused": "재검사를 요청하기 전에 이 소스를 재개하세요.", + "rescanFailed": "재검사를 예약하지 못했습니다", + "retryRejected": "다시 수집하고 검토", + "retryRejectedPending": "검토 재시도를 예약했습니다", + "retryRejectedScheduled": "이 스킬을 다시 수집하여 동기화 검토로 보냅니다.", + "retryRejectedFailed": "검토 재시도를 예약하지 못했습니다", + "skillPaths": "스킬 경로", + "path": "경로", + "boundSkillId": "연결된 스킬 ID", + "acquisition": "수집 결과", + "ingestion": "가져오기 상태", + "review": "검토 상태", + "retryAt": "재시도 시각", + "runId": "실행 ID", + "runDetail": { + "title": "실행 상세 정보", + "back": "실행 목록으로 돌아가기", + "tasks": "저장소 작업", + "skillPaths": "스킬 경로 결과", + "repository": "저장소", + "taskStatus": "작업 상태", + "taskMode": "확인 방식", + "taskKind": "작업 유형", + "pathOutcome": "수집 결과", + "ingestionStatus": "수신 처리 상태", + "reviewStatus": "검토 상태", + "retryAt": "다음 재시도", + "noTasks": "이 실행에 기록된 작업이 없습니다", + "noPaths": "이 실행에 기록된 스킬 경로가 없습니다", + "loading": "실행 상세 정보를 불러오는 중…", + "loadFailed": "실행 상세 정보를 불러오지 못했습니다", + "taskCount": "작업", + "pathCount": "경로", + "pagePrevious": "이전 페이지", + "pageNext": "다음 페이지", + "discoveryNoPaths": "탐색 실행에는 스킬 경로 결과가 아닌 저장소 단서가 기록됩니다." + }, + "runKind": "실행 유형", + "runTarget": "대상", + "runMultipleTargets": "대상 {{count}}개 · 작업 확인", + "tasks": "저장소 작업", + "createdAt": "생성됨", + "close": "닫기", + "loading": "불러오는 중…", + "loadFailed": "불러오기 실패", + "empty": "아직 기록이 없습니다", + "codes": { + "ACTIVE": "활성", + "PAUSED": "일시 중지됨", + "UNREACHABLE": "접근 불가", + "INVALID": "인증 유효하지 않음", + "DISABLED": "비활성화됨", + "UNKNOWN": "전체 검사 대기 중", + "PRESENT": "마지막 전체 검사에서 발견됨", + "MISSING": "마지막 전체 검사에서 발견되지 않음", + "SEARCH_RULE": "검색 규칙", + "CURATED_OWNER": "지정된 계정", + "CURATED_REPOSITORY": "지정된 저장소", + "DISCOVERY": "탐색", + "REPOSITORY_SYNC": "저장소 확인 및 수집", + "QUEUED": "대기열 등록됨", + "LEASED": "실행 중", + "RETRY_WAIT": "재시도 대기 중", + "FAILED": "실패", + "PARTIAL": "일부 완료", + "ACQUISITION_COMPLETE": "수집 완료", + "RECEIVED": "수신됨", + "VALIDATING": "검증 중", + "VALIDATED": "검증됨", + "READY_FOR_REVIEW": "동기화 검토 대기", + "BLOCKED": "차단됨", + "APPROVED": "승인됨", + "REJECTED": "반려됨", + "SUPERSEDED": "대체됨", + "PENDING": "검토 대기", + "ACCEPTED": "접수됨", + "IGNORED": "무시됨", + "UNCHANGED": "변경 없음", + "RETRYABLE_FAILURE": "재시도 가능한 실패", + "TERMINAL_FAILURE": "최종 실패", + "PATH_TERMINAL_FAILURE": "일부 스킬 경로 수집에 실패했습니다", + "NO_CHANGE": "변경 없음", + "GITHUB_RATE_LIMITED": "GitHub 요청 제한에 도달했습니다", + "AUTH_INVALID": "GitHub 자격 증명이 유효하지 않습니다", + "SOURCE_UNREADABLE": "저장소를 읽을 수 없습니다", + "BACKPRESSURE": "대기 항목 용량에 도달했습니다", + "CAPACITY": "작업 공간 용량이 부족합니다", + "LOCAL_DISK_BUDGET": "임시 디스크 공간이 부족합니다", + "SLUG_CONFLICT": "스킬 이름 충돌", + "PATH_MOVE_SCAN_INCOMPLETE": "검사 미완료: 이후 재검사 대기 중", + "PATH_MOVE_AMBIGUOUS": "경로 변경에 수동 확인이 필요합니다", + "PATH_MOVE_STALE_SNAPSHOT": "이전 검사가 대체되었습니다. 최신 스냅샷을 기다립니다", + "SKILL_ROOT_NOT_FOUND": "스킬 디렉터리를 찾지 못했습니다", + "SKILL_PATH_IGNORED": "스킬 경로가 무시되었습니다", + "TREE_INCOMPLETE": "저장소 트리 검사 미완료", + "DUPLICATE_PATH": "저장소에 중복 경로가 있습니다", + "RESERVED_EVIDENCE_PATH": "예약된 라이선스 근거 경로가 사용되었습니다", + "SUBMODULE": "스킬에 Git 서브모듈이 포함되어 있습니다", + "SYMLINK": "스킬에 심볼릭 링크가 포함되어 있습니다", + "UNSUPPORTED_ENTRY": "스킬에 지원하지 않는 파일 유형이 포함되어 있습니다", + "INVALID_BLOB_METADATA": "파일 메타데이터가 올바르지 않습니다", + "PACKAGE_TOO_LARGE": "스킬 패키지가 파일 수 또는 크기 제한을 초과합니다", + "MISSING_SKILL_MD": "SKILL.md가 없습니다", + "TOO_MANY_ROOT_EVIDENCE_FILES": "저장소 루트의 라이선스 근거 파일이 너무 많습니다", + "UNSAFE_ROOT_EVIDENCE": "저장소 루트의 라이선스 근거가 안전하지 않습니다", + "PACKAGE_ACQUISITION_FAILED": "스킬 패키지를 가져오지 못했습니다", + "ARCHIVE_TOO_LARGE": "압축 파일이 업로드 크기 제한을 초과합니다", + "PACKAGE_DELIVERY_FAILED": "스킬 패키지 업로드 실패", + "PACKAGE_LIMIT": "스킬 패키지가 제한을 초과합니다", + "ARCHIVE_SIZE_MISMATCH": "압축 파일 크기가 보고된 값과 다릅니다", + "ARCHIVE_DIGEST_MISMATCH": "압축 파일 해시가 보고된 값과 다릅니다", + "PACKAGE_FILE_LIMIT": "스킬 패키지에 파일이 너무 많습니다", + "PACKAGE_PATH_INVALID": "스킬 패키지 파일 경로가 올바르지 않습니다", + "PACKAGE_STRUCTURE_INVALID": "스킬 패키지 구조가 올바르지 않습니다", + "LICENSE_EVIDENCE_INVALID": "라이선스 근거가 올바르지 않습니다", + "LICENSE_EVIDENCE_LIMIT": "라이선스 근거가 제한을 초과합니다", + "PACKAGE_SIZE_LIMIT": "스킬 패키지 콘텐츠가 크기 제한을 초과합니다", + "PACKAGE_POLICY_INVALID": "스킬 패키지가 현재 정책을 위반합니다", + "PACKAGE_CONTENT_CHANGED": "스킬 패키지 콘텐츠가 저장소 메타데이터와 다릅니다", + "INVALID_ARCHIVE": "압축 파일이 올바르지 않습니다", + "NO_VALID_SKILL": "저장소에서 유효한 스킬을 찾지 못했습니다", + "REPOSITORY_FAILED": "저장소 확인 실패", + "RETRYABLE_ACQUISITION": "수집 실패: 재시도 대기 중", + "INCOMPLETE_TREE": "저장소 트리 검사 미완료", + "INTERNAL_FAILURE": "내부 처리 실패", + "BUDGET": "검사 예산 소진", + "RATE_LIMIT": "GitHub 요청 한도 도달", + "SOURCE_NOT_BOUND": "저장소 소스가 연결되지 않음", + "SOURCE_PAUSED": "저장소 소스 일시 중지됨" + } + }, + "overview": { + "mySkills": "내 스킬", + "mySkillsDesc": "게시한 모든 스킬을 확인하고 관리하세요", + "mySuites": "내 스킬 모음", + "mySuitesDesc": "내 스킬 모음을 확인하고 관리하세요", + "publish": "게시", + "publishDesc": "새 스킬을 업로드하고 게시하세요", + "tokens": "API 토큰", + "tokensDesc": "API 접근 토큰을 관리하세요", + "stars": "내 즐겨찾기", + "starsDesc": "즐겨찾기하거나 평점을 남긴 스킬을 확인하세요", + "profile": "프로필", + "profileDesc": "표시 이름과 개인 정보를 편집하세요", + "security": "보안", + "securityDesc": "비밀번호와 계정 보안 옵션을 변경하세요" + }, + "mySkills": { + "ownerReadOnly": "더 이상 이 공간의 멤버가 아닙니다. 내 스킬을 조회하고 다운로드할 수 있지만 수정할 수는 없습니다.", + "updateGlobalVersion": "글로벌 버전 업데이트", + "viewGlobalCopy": "글로벌 사본 보기", + "viewTeamSource": "팀 원본 보기", + "promotionRoles": { + "TEAM_SOURCE": "팀 원본 · 글로벌로 승격됨", + "GLOBAL_COPY": "글로벌 사본 · 독립적으로 업데이트됨" + }, + "versionDifference": "이번 업데이트 v{{version}} · 공개 버전 v{{published}}", + "statusUpdatePendingReview": "업데이트 검토 대기 중", + "title": "내 스킬", + "subtitle": "게시한 스킬 관리", + "searchPlaceholder": "이름, 슬러그 또는 설명으로 검색", + "namespaceFilterLabel": "네임스페이스별 필터", + "namespaceFilterAll": "모든 네임스페이스", + "clearSearch": "필터 초기화", + "emptySearchTitle": "일치하는 스킬 없음", + "emptySearchDescription": "검색어를 변경하거나 다른 네임스페이스를 선택하세요.", + "filters": { + "ALL": "전체", + "PENDING_REVIEW": "검토 대기", + "PUBLISHED": "게시됨", + "REJECTED": "반려됨", + "ARCHIVED": "보관됨", + "HIDDEN": "숨김" + }, + "publishNew": "새 스킬 게시", + "update": "업데이트", + "archive": "보관", + "unarchive": "복원", + "statusArchived": "보관됨", + "statusPendingReview": "검토 대기", + "statusPublished": "게시됨", + "statusRejected": "반려됨", + "statusHidden": "숨김", + "statusScanning": "검사 중", + "statusScanFailed": "검사 실패", + "archiveConfirmTitle": "스킬 보관", + "archiveConfirmDescription": "보관하면 일반 사용자가 \"{{skill}}\"을 보거나 다운로드할 수 없습니다. 계속하시겠습니까?", + "unarchiveConfirmTitle": "스킬 복원", + "unarchiveConfirmDescription": "\"{{skill}}\"을 복원하면 다시 표시되며 새 버전을 게시할 수 있습니다.", + "archiveSuccessTitle": "스킬을 보관했습니다", + "archiveSuccessDescription": "\"{{skill}}\"을 보관했습니다.", + "archiveErrorTitle": "스킬을 보관하지 못했습니다", + "unarchiveSuccessTitle": "스킬을 복원했습니다", + "unarchiveSuccessDescription": "\"{{skill}}\"을 복원했습니다. 다시 새 버전을 게시할 수 있습니다.", + "unarchiveErrorTitle": "스킬을 복원하지 못했습니다", + "restoreHidden": "숨김 해제", + "restoreHiddenConfirmTitle": "숨겨진 스킬 복원", + "restoreHiddenConfirmDescription": "\"{{skill}}\"이 일반 서비스 화면에 다시 표시됩니다.", + "restoreHiddenSuccessTitle": "스킬 숨김을 해제했습니다", + "restoreHiddenSuccessDescription": "\"{{skill}}\"을 숨겨진 스킬 목록에서 제거했습니다.", + "restoreHiddenErrorTitle": "숨겨진 스킬을 복원하지 못했습니다", + "withdrawReview": "검토 철회", + "withdrawConfirmTitle": "업로드 철회", + "withdrawConfirmDescription": "철회하면 \"{{skill}}\"의 검토가 중단되며 대기 중인 버전이 삭제됩니다.", + "withdrawSuccessTitle": "업로드를 철회했습니다", + "withdrawSuccessDescription": "\"{{skill}}\"의 대기 중인 버전을 철회했습니다.", + "withdrawErrorTitle": "업로드를 철회하지 못했습니다", + "promoteToGlobal": "글로벌로 승격", + "submitVersionToGlobal": "글로벌에 버전 제출", + "promotionConfirmTitle": "승격 요청 제출", + "promotionConfirmDescription": "\"{{skill}}\"의 v{{version}}을 글로벌 네임스페이스로 승격 요청하시겠습니까?", + "promotionSuccessTitle": "승격 요청을 제출했습니다", + "promotionSuccessDescription": "\"{{skill}}\"의 v{{version}}이 글로벌 승격 검토 대기열에 등록되었습니다.", + "promotionDuplicateTitle": "이미 승격 대기 중", + "promotionDuplicateDescription": "이 버전에는 이미 대기 중인 승격 요청이 있습니다.", + "promotionAlreadyPromotedTitle": "이미 승격된 스킬", + "promotionAlreadyPromotedDescription": "이 스킬은 이미 글로벌 네임스페이스로 승격되었습니다.", + "promotionErrorTitle": "승격 요청을 제출하지 못했습니다", + "emptyTitle": "아직 스킬이 없습니다", + "emptyDescription": "첫 스킬을 게시해 보세요", + "emptyFilteredTitle": "이 필터에 맞는 스킬이 없습니다", + "emptyFilteredDescription": { + "PENDING_REVIEW": "현재 검토 대기 중인 스킬이 없습니다.", + "PUBLISHED": "현재 게시된 스킬이 없습니다.", + "REJECTED": "현재 반려된 스킬이 없습니다.", + "ARCHIVED": "현재 보관된 스킬이 없습니다.", + "HIDDEN": "현재 숨겨진 스킬이 없습니다." + }, + "publishSkill": "스킬 게시" + }, + "myNamespaces": { + "title": "내 네임스페이스", + "subtitle": "네임스페이스와 팀을 관리하세요", + "create": "네임스페이스 만들기", + "creating": "생성하는 중...", + "createSubmit": "지금 만들기", + "createDialogTitle": "팀 네임스페이스 만들기", + "createDialogDescription": "생성 후 소유자(OWNER)가 되며 멤버, 검토, 스킬 게시를 관리할 수 있습니다.", + "createSlugLabel": "네임스페이스 슬러그", + "createSlugPlaceholder": "예: team-ml", + "createSlugHint": "영문 소문자, 숫자, 하이픈으로 구성된 2~64자를 사용하세요. 처음과 끝에는 하이픈을 사용할 수 없습니다.", + "createSlugRequired": "네임스페이스 슬러그를 입력하세요", + "createSlugLength": "네임스페이스 슬러그는 {{min}}~{{max}}자여야 합니다", + "createSlugPattern": "네임스페이스 슬러그에는 영문 소문자, 숫자, 하이픈만 사용할 수 있습니다", + "createSlugDoubleHyphen": "네임스페이스 슬러그에는 하이픈을 연속으로 사용할 수 없습니다", + "createSlugReserved": "\"{{slug}}\"는 예약된 이름입니다. 다른 슬러그를 선택하세요.", + "createDisplayNameLabel": "표시 이름", + "createDisplayNamePlaceholder": "예: 머신러닝 팀", + "createDisplayNameRequired": "표시 이름을 입력하세요", + "createDisplayNameLength": "표시 이름은 {{max}}자 이하여야 합니다", + "createDescriptionLabel": "설명", + "createDescriptionPlaceholder": "이 네임스페이스의 목적, 사용자 또는 관리 방식을 간단히 설명하세요", + "createDescriptionHint": "선택 사항이며 최대 512자입니다.", + "createDescriptionLength": "설명은 {{max}}자 이하여야 합니다", + "createSuccessTitle": "네임스페이스를 만들었습니다", + "createSuccessDescription": "\"{{name}}\"이 준비되었습니다. 멤버를 관리하거나 스킬을 게시할 수 있습니다.", + "createErrorTitle": "네임스페이스를 만들지 못했습니다", + "typeGlobal": "글로벌", + "typeTeam": "팀", + "roleLabel": "현재 역할", + "roleUnknown": "알 수 없음", + "manageMembers": "멤버 관리", + "reviewTasks": "검토 작업", + "freeze": "동결", + "unfreeze": "동결 해제", + "archive": "보관", + "restore": "복원", + "delete": "삭제", + "activeHint": "이 네임스페이스는 활성 상태이며 멤버, 검토, 스킬 게시를 계속 관리할 수 있습니다.", + "frozenHint": "이 네임스페이스는 동결되었습니다. 멤버가 조회할 수 있지만 읽기 전용이므로 게시하거나 멤버를 변경할 수 없습니다.", + "archivedHint": "이 네임스페이스는 보관되었습니다. 공개 진입점에서는 숨겨지지만 대시보드에서 조회하고 복원할 수 있습니다.", + "immutableHint": "기본 제공 시스템 네임스페이스이며 항상 읽기 전용입니다.", + "freezeConfirmTitle": "네임스페이스 동결", + "freezeConfirmDescription": "\"{{name}}\"을 동결하면 읽기 전용으로 전환됩니다. 계속하시겠습니까?", + "unfreezeConfirmTitle": "네임스페이스 동결 해제", + "unfreezeConfirmDescription": "\"{{name}}\"의 동결을 해제하면 정상적으로 관리하고 게시할 수 있습니다.", + "archiveConfirmTitle": "네임스페이스 보관", + "archiveConfirmDescription": "\"{{name}}\"이 공개 진입점에서 숨겨지며 대시보드에서만 복원할 수 있습니다.", + "restoreConfirmTitle": "네임스페이스 복원", + "restoreConfirmDescription": "\"{{name}}\"이 활성 상태로 돌아갑니다.", + "deleteConfirmTitle": "네임스페이스 삭제", + "deleteConfirmDescription": "\"{{name}}\"을 영구 삭제합니다. 이 작업은 되돌릴 수 없습니다.", + "freezeSuccessTitle": "네임스페이스를 동결했습니다", + "freezeSuccessDescription": "\"{{name}}\"이 읽기 전용으로 전환되었습니다.", + "freezeErrorTitle": "네임스페이스를 동결하지 못했습니다", + "unfreezeSuccessTitle": "네임스페이스의 동결을 해제했습니다", + "unfreezeSuccessDescription": "\"{{name}}\"이 다시 활성화되었습니다.", + "unfreezeErrorTitle": "네임스페이스의 동결을 해제하지 못했습니다", + "archiveSuccessTitle": "네임스페이스를 보관했습니다", + "archiveSuccessDescription": "\"{{name}}\"이 공개 진입점에서 숨겨졌습니다.", + "archiveErrorTitle": "네임스페이스를 보관하지 못했습니다", + "restoreSuccessTitle": "네임스페이스를 복원했습니다", + "restoreSuccessDescription": "\"{{name}}\"을 다시 정상적으로 운영할 수 있습니다.", + "restoreErrorTitle": "네임스페이스를 복원하지 못했습니다", + "deleteSuccessTitle": "네임스페이스를 삭제했습니다", + "deleteSuccessDescription": "\"{{name}}\"을 영구 삭제했습니다.", + "deleteErrorTitle": "네임스페이스를 삭제하지 못했습니다", + "emptyTitle": "아직 네임스페이스가 없습니다", + "emptyDescription": "네임스페이스를 만들어 스킬을 정리하세요" + }, + "tokens": { + "pageTitle": "토큰 관리", + "pageSubtitle": "CLI와 API의 접근 자격 증명을 관리하세요" + }, + "theme": { + "darkMode": "어두운 테마", + "switchToLight": "밝은 테마로 전환", + "switchToDark": "어두운 테마로 전환" + }, + "reviewProgress": { + "title": "내 검토 진행 상황", + "subtitle": "스킬과 스킬 모음의 현재 결정, 검토자 의견, 모든 재제출 내역을 확인하세요", + "searchLabel": "스킬, 스킬 모음 또는 네임스페이스 검색", + "searchPlaceholder": "스킬, 스킬 모음 또는 네임스페이스 검색", + "searchAction": "검색", + "typeFilter": "리소스 유형별 필터", + "typeAll": "모든 유형", + "typeSkill": "스킬", + "typeSuite": "스킬 모음", + "statusFilter": "검토 상태별 필터", + "statusAll": "모든 상태", + "statusPending": "검토 중", + "statusApproved": "승인됨", + "statusRejected": "반려됨", + "statusSummary": "현재 검토 상태 요약", + "latestSubmitted": "최근 제출: {{time}}", + "latestReviewed": "최근 결정: {{time}}", + "attemptCount": "제출 {{count}}회", + "attemptNumber": "{{number}}차 제출", + "submittedAt": "제출 시각 {{time}}", + "reviewedAt": "검토 시각 {{time}}", + "reviewedBy": "검토자 {{reviewer}}", + "history": "제출 내역", + "resubmit": "수정 후 재제출", + "loading": "검토 진행 상황을 불러오는 중", + "error": "검토 진행 상황을 불러오지 못했습니다. 나중에 다시 시도하세요.", + "historyError": "제출 내역을 불러오지 못했습니다. 나중에 다시 시도하세요.", + "emptyTitle": "아직 검토 내역이 없습니다", + "emptyDescription": "공개 또는 팀 스킬과 스킬 모음을 제출하면 검토 내역이 여기에 표시됩니다." + }, + "reviews": { + "title": "검토 센터", + "subtitle": "플랫폼 검토 작업을 관리하세요", + "typeSkill": "스킬 검토", + "typeSync": "GitHub 동기화 검토", + "syncTitle": "GitHub 동기화 검토 대기열", + "syncDescription": "이 페이지에서는 저장소와 수집 실행별로 묶어 표시합니다. 각 스킬을 개별적으로 검토하세요.", + "syncRun": "실행", + "syncGroupCount": "스킬 {{count}}개", + "syncOpenReview": "검토 열기", + "syncEmpty": "이 대기열에 동기화 검토가 없습니다", + "syncError": "동기화 검토 대기열을 불러오지 못했습니다. 나중에 다시 시도하세요.", + "tabSuperseded": "대체됨", + "typeProfile": "프로필 검토", + "tabPending": "대기 중", + "tabApproved": "승인됨", + "tabRejected": "반려됨", + "empty": "검토 작업 없음", + "colSkill": "스킬", + "colVersion": "버전", + "colSubmitter": "제출자", + "colSubmitTime": "제출 시각", + "colReviewer": "검토자", + "colReviewTime": "검토 시각", + "sortLabel": "시간순 정렬", + "sortNewest": "최신순", + "sortOldest": "오래된 순", + "pageSummary": "총 {{total}}개 기록, {{page}}페이지", + "prevPage": "이전", + "nextPage": "다음" + }, + "profileReview": { + "queueTitle": "프로필 검토 대기열", + "queueSubtitle": "상태 필터, 페이지 이동, 검토 작업으로 표시 이름 변경 요청을 처리하세요.", + "empty": "프로필 검토 작업 없음", + "colUser": "사용자", + "colCurrentName": "현재 이름", + "colRequestedName": "요청한 이름", + "colSubmittedAt": "제출 시각", + "colMachineResult": "자동 검토", + "colActions": "작업", + "colReviewInfo": "검토 정보", + "approve": "승인", + "reject": "반려", + "approveSuccess": "프로필 변경을 승인했습니다", + "approveFailed": "승인하지 못했습니다", + "rejectSuccess": "프로필 변경을 반려했습니다", + "rejectFailed": "반려하지 못했습니다", + "confirmApproveTitle": "승인 확인", + "confirmApproveDesc": "\"{{from}}\"에서 \"{{to}}\"로 변경하는 것을 승인하시겠습니까?", + "confirmRejectTitle": "변경 반려", + "confirmRejectDesc": "반려 사유를 입력하세요.", + "rejectPlaceholder": "반려 사유...", + "userId": "사용자 ID", + "reviewer": "검토자", + "comment": "의견", + "totalItems": "총 {{total}}개 프로필 검토 기록", + "sortLabel": "시간순 정렬", + "sortNewest": "최신순", + "sortOldest": "오래된 순", + "pageSummary": "총 {{total}}개 기록, {{page}}페이지", + "prevPage": "이전", + "nextPage": "다음", + "tabPending": "대기 중", + "tabApproved": "승인됨", + "tabRejected": "반려됨" + }, + "stars": { + "title": "내 즐겨찾기", + "subtitle": "즐겨찾기한 스킬을 확인하세요", + "empty": "아직 즐겨찾기한 스킬이 없습니다" + }, + "subscriptions": { + "title": "내 구독", + "subtitle": "업데이트를 구독한 스킬", + "empty": "아직 구독한 스킬이 없습니다." + }, + "promotions": { + "title": "승격 검토", + "kindInitial": "최초 승격", + "kindUpdate": "글로벌 스킬 업데이트", + "targetVersionTag": "현재 글로벌 v{{version}}", + "updateReviewHint": "승인하면 기존 글로벌 스킬에 버전이 추가됩니다. 원본과 현재 글로벌 버전을 확인하세요.", + "subtitle": "팀 스킬의 글로벌 네임스페이스 승격 요청을 검토하세요", + "tabPending": "대기 중", + "tabRevocations": "검토할 승격 취소 요청", + "tabRevocationHistory": "승격 취소 내역", + "revocationEmpty": "승격 취소 요청 없음", + "revocationHistoryEmpty": "검토된 승격 취소 요청 없음", + "revocationStatus": { + "PENDING": "대기 중", + "APPROVED": "취소됨", + "REJECTED": "반려됨" + }, + "revocationActors": "요청자 {{submitter}} · 검토자 {{reviewer}}", + "revocationReviewComment": "검토 의견: {{comment}}", + "revocationLoadError": "승격 취소 요청을 불러오지 못했습니다. 다시 시도하세요.", + "revocationIds": "원본 스킬 #{{source}} · 글로벌 스킬 #{{target}}", + "revocationReason": "사유: {{reason}}", + "approveRevocation": "승격 취소 승인", + "rejectRevocation": "승격 취소 반려", + "revocationConfirmTitle": "승격 취소 승인 확인", + "revocationConfirmDescription": "승인하면 @global/{{slug}}의 모든 글로벌 버전, 평점, 통계가 삭제됩니다. 주소는 재사용될 수 있습니다. 팀 스킬은 유지되며 설치된 파일은 회수할 수 없습니다.", + "revocationApproved": "승격 취소를 승인했습니다", + "revocationRejected": "승격 취소 요청을 반려했습니다", + "revocationActionError": "승격 취소 요청을 처리하지 못했습니다", + "tabApproved": "승인됨", + "tabRejected": "반려됨", + "commentPlaceholder": "검토 의견(선택 사항)", + "approve": "승인", + "reject": "반려", + "empty": "승격 요청 없음", + "historyTableLabel": "승격 내역", + "colSkill": "스킬", + "colVersion": "버전", + "colSubmitter": "제출자", + "colReviewer": "검토자", + "colReviewedAt": "검토 시각", + "colReviewComment": "검토 의견", + "sortReviewedTimeAsc": "검토 시각 오름차순", + "sortReviewedTimeDesc": "검토 시각 내림차순", + "emptyValue": "-", + "versionTag": "v{{version}}", + "submitterTag": "제출자 {{user}}", + "fileCountTag": "파일 {{count}}개", + "packageSizeTag": "{{size}}", + "downloadCountTag": "다운로드 {{value}}회", + "starCountTag": "즐겨찾기 {{value}}개" + }, + "adminUsers": { + "title": "사용자 관리", + "subtitle": "플랫폼 사용자와 권한을 관리하세요", + "searchLabel": "사용자 검색", + "searchPlaceholder": "사용자 이름 또는 이메일 검색...", + "searchHint": "사용자 이름이나 이메일을 입력한 후 Enter 키를 누르거나 검색을 클릭하세요.", + "searchAction": "검색", + "clearSearch": "지우기", + "filterLabel": "상태 필터", + "filterAll": "전체", + "filterActive": "활성", + "filterPending": "대기 중", + "filterDisabled": "비활성화됨", + "empty": "사용자 데이터 없음", + "colUsername": "사용자 이름", + "colUserId": "사용자 ID", + "copyUserId": "{{username}}의 사용자 ID 복사", + "colEmail": "이메일", + "colStatus": "상태", + "colRole": "역할", + "colCreatedAt": "생성 시각", + "colActions": "작업", + "statusActive": "활성", + "statusPending": "대기 중", + "statusDisabled": "비활성화됨", + "changeRole": "역할 변경", + "approveUser": "승인", + "disable": "비활성화", + "enable": "활성화", + "resetPassword": "비밀번호 재설정", + "totalRecords": "총 {{total}}개 기록, {{page}}페이지", + "prevPage": "이전", + "nextPage": "다음", + "changeRoleTitle": "사용자 역할 변경", + "changeRoleDesc": "사용자 {{username}}에게 새 역할 할당", + "roleLabel": "역할", + "selectRole": "역할 선택", + "roleUser": "사용자", + "roleReviewer": "검토자", + "roleUserAdmin": "사용자 관리자", + "roleAuditor": "감사자", + "roleSuperAdmin": "최고 관리자", + "confirmAction": "작업 확인", + "confirmDisable": "사용자 {{username}}을 비활성화하시겠습니까?", + "confirmEnable": "사용자 {{username}}을 활성화하시겠습니까?", + "confirmResetPassword": "사용자 {{username}}에게 비밀번호 재설정 인증 코드를 보내시겠습니까?" + }, + "adminLabels": { + "title": "라벨 관리", + "subtitle": "라벨 정의, 공개 범위, 번역, 필터 순서를 관리하세요.", + "createAction": "라벨 만들기", + "editAction": "편집", + "deleteAction": "삭제", + "saveAction": "변경 사항 저장", + "cancelAction": "취소", + "moveUp": "위로 이동", + "moveDown": "아래로 이동", + "empty": "아직 정의된 라벨이 없습니다.", + "summaryDefinitionsTitle": "정의", + "summaryVisibleTitle": "표시되는 필터", + "summaryPrivilegedTitle": "권한 제한 라벨", + "colLabel": "라벨", + "colType": "유형", + "colVisibility": "공개 범위", + "colSortOrder": "정렬 순서", + "colTranslations": "번역", + "colCreatedAt": "생성 시각", + "colActions": "작업", + "typeRecommended": "추천", + "typePrivileged": "권한 제한", + "visibilityVisible": "필터에 표시", + "visibilityHidden": "필터에서 숨김", + "createDialogTitle": "라벨 만들기", + "createDialogDescription": "새 라벨과 검색 및 상세 화면에 표시할 번역을 정의하세요.", + "editDialogTitle": "라벨 편집", + "editDialogDescription": "이 라벨 정의의 표시 규칙과 번역을 업데이트하세요.", + "deleteDialogTitle": "라벨 삭제", + "deleteDialogDescription": "라벨 \"{{slug}}\"를 삭제하고 연결된 모든 스킬에서 제거합니다.", + "formSlug": "슬러그", + "formType": "유형", + "formVisibility": "공개 범위", + "formSortOrder": "정렬 순서", + "formTranslations": "번역", + "formTranslationsHint": "언어 코드와 표시 이름을 한 쌍 이상 입력하세요.", + "translationLocalePlaceholder": "언어 코드(예: ko 또는 en)", + "translationDisplayNamePlaceholder": "표시 이름", + "addTranslation": "번역 추가", + "removeTranslation": "제거", + "validationSlugTitle": "슬러그 필요", + "validationSlugDescription": "라벨을 저장하기 전에 고정된 식별자로 사용할 슬러그를 입력하세요.", + "validationSlugPatternDescription": "영문 소문자, 숫자, 단일 하이픈만 사용하세요. 슬러그는 영문자 또는 숫자로 시작하고 끝나야 합니다.", + "validationTranslationsTitle": "번역 필요", + "validationTranslationsDescription": "언어 코드와 표시 이름을 모두 입력한 쌍이 하나 이상 필요합니다.", + "validationDuplicateLocaleDescription": "각 번역 언어 코드는 한 번만 사용할 수 있습니다.", + "createSuccessTitle": "라벨을 만들었습니다", + "createSuccessDescription": "이제 라벨 정의를 사용할 수 있습니다.", + "createErrorTitle": "라벨을 만들지 못했습니다", + "updateSuccessTitle": "라벨을 업데이트했습니다", + "updateSuccessDescription": "라벨 정의를 업데이트했습니다.", + "updateErrorTitle": "라벨을 업데이트하지 못했습니다", + "deleteSuccessTitle": "라벨을 삭제했습니다", + "deleteSuccessDescription": "라벨 정의를 제거했습니다.", + "deleteErrorTitle": "라벨을 삭제하지 못했습니다", + "sortSuccessTitle": "정렬 순서를 업데이트했습니다", + "sortSuccessDescription": "라벨 순서를 저장했습니다.", + "sortErrorTitle": "정렬 순서를 업데이트하지 못했습니다", + "fallbackErrorDescription": "라벨 작업을 완료하지 못했습니다." + }, + "auditLog": { + "title": "감사 로그", + "subtitle": "시스템 작업 기록을 확인하세요", + "filterAll": "전체", + "filterCliPublish": "CLI 게시", + "filterCompatPublish": "호환 API 게시", + "filterReviewSubmit": "검토 제출", + "filterReviewApprove": "검토 승인", + "filterReviewReject": "검토 반려", + "filterPromotionSubmit": "승격 제출", + "filterPromotionApprove": "승격 승인", + "filterPromotionReject": "승격 반려", + "filterReportSkill": "스킬 신고", + "filterResolveSkillReport": "신고 처리", + "filterDismissSkillReport": "신고 기각", + "filterHideSkill": "스킬 숨김", + "filterArchiveSkill": "스킬 보관", + "filterUnhideSkill": "스킬 숨김 해제", + "filterUnarchiveSkill": "스킬 복원", + "filterYankVersion": "버전 배포 철회", + "filterRebuildSearchIndex": "검색 인덱스 재구축", + "quickFilterSearchRebuild": "검색 인덱스 재구축만", + "clearFilters": "필터 초기화", + "userIdPlaceholder": "사용자 ID...", + "requestIdPlaceholder": "요청 ID...", + "ipPlaceholder": "IP 주소...", + "resourceTypePlaceholder": "리소스 유형...", + "resourceIdPlaceholder": "리소스 ID...", + "empty": "감사 로그 없음", + "colTime": "시각", + "colAction": "작업", + "colUserId": "사용자 ID", + "colUsername": "사용자 이름", + "colIp": "IP 주소", + "colDetail": "상세 정보", + "totalRecords": "총 {{total}}개 기록, {{page}}페이지", + "prevPage": "이전", + "nextPage": "다음" + }, + "profile": { + "title": "프로필 설정", + "subtitle": "표시 이름과 개인 정보를 관리하세요.", + "displayName": "표시 이름", + "email": "이메일", + "resetPassword": "비밀번호 재설정", + "edit": "편집", + "save": "저장", + "saving": "저장하는 중...", + "cancel": "취소", + "successTitle": "프로필을 업데이트했습니다", + "successDescription": "표시 이름을 업데이트했습니다.", + "pendingReviewTitle": "검토를 요청했습니다", + "pendingReviewDescription": "표시 이름 변경이 검토 대기 중이며 승인되면 적용됩니다.", + "defaultError": "프로필을 업데이트하지 못했습니다. 다시 시도하세요.", + "validation": { + "length": "표시 이름은 2~32자여야 합니다.", + "pattern": "표시 이름에는 한자, 영문자, 숫자, 공백, 밑줄, 하이픈만 사용할 수 있습니다." + }, + "pendingReview": "표시 이름을 \"{{name}}\"으로 변경하는 요청이 검토 대기 중입니다.", + "rejected": "표시 이름 변경 요청이 반려되었습니다.", + "rejectedReason": "사유: {{reason}}", + "reviewHint": "일부 변경 사항은 적용 전에 관리자 검토가 필요합니다.", + "partiallyAppliedTitle": "일부 적용됨", + "partiallyAppliedDescription": "일부 변경 사항은 즉시 적용되었으며 나머지는 관리자 검토 대기 중입니다.", + "noChanges": "저장할 변경 사항이 없습니다.", + "userId": "사용자 ID" + }, + "security": { + "title": "보안 설정", + "subtitle": "로컬 계정 로그인이 활성화된 경우 비밀번호를 변경할 수 있습니다.", + "currentPassword": "현재 비밀번호", + "newPassword": "새 비밀번호", + "currentPasswordRequired": "현재 비밀번호를 입력하세요", + "newPasswordRequired": "새 비밀번호를 입력하세요", + "invalidCurrentPassword": "현재 비밀번호가 올바르지 않습니다", + "success": "비밀번호를 변경했습니다", + "successTitle": "비밀번호를 변경했습니다", + "successDescription": "새 비밀번호로 다시 로그인하세요.", + "defaultError": "비밀번호를 변경하지 못했습니다", + "unavailableTitle": "이 계정에서는 비밀번호를 변경할 수 없습니다.", + "unavailableDescription": "이 계정은 외부 인증 제공자를 통해 로그인하거나 로컬 비밀번호 자격 증명이 없습니다.", + "submitting": "제출하는 중...", + "submit": "비밀번호 변경" + }, + "accounts": { + "initiateTitle": "계정 병합 시작", + "initiateDesc": "보조 계정 식별자를 입력하세요. 로컬 사용자 이름 또는 외부 인증의 `provider:subject` 형식을 지원합니다.", + "secondaryLabel": "보조 계정 식별자", + "secondaryPlaceholder": "예: other_user 또는 github:123456", + "initiating": "시작하는 중...", + "initiate": "병합 시작", + "initiateSuccess": "병합 요청을 만들었습니다. 요청 ID를 저장한 다음 다른 브라우저에서 {{secondaryUserId}} 계정으로 로그인하여 승인하세요.", + "initiateError": "병합을 시작하지 못했습니다", + "verifyTitle": "두 번째 계정에서 승인", + "verifyDesc": "다른 브라우저에서 두 번째 계정으로 로그인하고 요청 ID를 입력한 다음 대상 계정을 확인하여 승인하세요.", + "mergeRequestId": "병합 요청 ID", + "inspect": "대상 계정 보기", + "inspectError": "병합 요청을 조회할 수 없습니다", + "destinationWarning": "승인하면 이 계정의 로그인 방식, API 토큰, 권한이 {{name}}({{id}}) 계정으로 이전되고 이 계정은 비활성화됩니다. 두 계정 모두 본인 소유인지 확인하세요.", + "verifying": "확인하는 중...", + "verify": "병합 승인", + "verifySuccess": "승인했습니다. 병합을 시작한 계정으로 돌아가 30분 이내에 확인하세요.", + "verifyError": "병합 검증 실패", + "confirming": "확인하는 중...", + "confirmDesc": "병합을 시작한 계정만 병합을 확정할 수 있습니다. 승인 후 해당 계정으로 돌아가세요.", + "confirm": "확인 및 병합 완료", + "confirmSuccess": "계정 병합 완료", + "confirmError": "병합 확인 실패", + "cancel": "병합 요청 취소", + "cancelSuccess": "병합 요청을 취소했습니다", + "cancelError": "병합 요청을 취소하지 못했습니다" + }, + "namespace": { + "notFound": "네임스페이스를 찾을 수 없습니다", + "skillList": "스킬", + "emptyTitle": "스킬 없음", + "emptyDescription": "이 네임스페이스에 아직 게시된 스킬이 없습니다" + }, + "skillDetail": { + "copyCoordinate": "전체 스킬 식별자 복사", + "unpublishedPreviewTitle": "미게시 버전 미리보기", + "unpublishedPreviewDescription": "기존 미리보기 권한으로 미게시 버전을 보고 있습니다. 일반 사용자는 계속 게시된 이름, 요약, 콘텐츠를 보게 됩니다.", + "githubSourceTitle": "GitHub 소스", + "githubSourceVersion": "게시된 v{{version}}의 소스", + "githubLicenseScopeREPOSITORY_ROOT": "저장소", + "githubLicenseScopeSKILL_ROOT": "스킬", + "back": "뒤로", + "notFound": "스킬을 찾을 수 없습니다", + "notFoundDesc": "이 스킬은 삭제되었거나 존재하지 않습니다", + "loginRequired": "로그인 필요", + "loginRequiredDesc": "비공개 스킬입니다. 상세 정보를 보려면 로그인하세요.", + "accessDenied": "접근 거부", + "accessDeniedDesc": "이 스킬을 볼 권한이 없습니다", + "tabReadme": "README", + "tabOverview": "개요", + "tabFiles": "파일", + "tabVersions": "버전", + "noReadme": "README 없음", + "readmeUnavailable": "README를 일시적으로 사용할 수 없습니다. 나중에 다시 시도하거나 이 스킬 버전을 다시 업로드하세요.", + "documentationSource": "출처: {{path}}", + "documentationUnavailableTitle": "문서를 사용할 수 없습니다", + "documentationUnavailable": "문서 파일을 불러오지 못했습니다. 파일 목록에서 패키지 내용을 확인할 수 있습니다.", + "packageLinkMissingTitle": "파일을 찾을 수 없습니다", + "packageLinkMissingDescription": "이 링크는 현재 스킬 버전에 포함되지 않은 파일을 가리킵니다.", + "authorLabel": "작성자 {{name}}", + "expandOverview": "전체 개요 펼치기", + "collapseOverview": "내용 접기", + "noDocumentationTitle": "패키지 문서 없음", + "noDocumentationDescription": "이 스킬 버전에는 읽을 수 있는 개요 파일이 없습니다.", + "noDocumentationHint": "실행 파일만 제공하는 스킬도 많습니다. 아래 파일 목록과 버전 상세 정보를 확인하세요.", + "summaryLabel": "요약", + "noFiles": "파일 없음", + "noVersions": "버전 없음", + "fileCount": "파일 {{count}}개", + "version": "버전", + "downloads": "다운로드", + "rating": "평점", + "ratingNone": "없음", + "namespaceLabel": "네임스페이스", + "loginToRate": "즐겨찾기와 평점을 남기려면 로그인하세요", + "install": "설치", + "suiteEntryTitle": "스킬 모음의 진입점으로 사용됨", + "suiteEntryDescription": "이 스킬은 다음 스킬 모음의 진입점입니다. 포함된 모든 스킬을 사용하려면 아래 스킬 모음을 설치하세요.", + "suiteEntryMemberCount": "전체 스킬 모음 보기(스킬 {{count}}개)", + "suiteMembershipTitle": "포함된 스킬 모음", + "suiteMembershipDescription": "현재 버전에 이 스킬이 포함되어 있고 접근할 수 있는 스킬 모음입니다.", + "suiteMembershipEmpty": "현재 접근 가능한 스킬 모음에 이 스킬이 포함되어 있지 않습니다.", + "suiteMembershipEntryRole": "진입점 스킬", + "suiteMembershipMemberRole": "멤버", + "suiteMembershipSiblingTitle": "이 모음의 다른 스킬", + "suiteMembershipRestricted": "접근이 제한되거나 삭제된 구성 스킬 {{count}}개", + "suiteMembershipOmitted": "펼치지 않은 표시 가능 구성 스킬 {{count}}개", + "suiteMembershipMore": "스킬 모음 소속 {{total}}개 중 {{shown}}개를 표시합니다. 나머지 결과는 페이지 단위 API로 확인할 수 있습니다.", + "suiteMembershipUnavailable": "현재 사용 불가", + "installMethodClawhub": "ClawHub CLI", + "installMethodSkillhub": "SkillHub CLI", + "installCommandUnsafeVersion": "이 버전은 여러 운영체제에서 안전하게 사용할 수 있는 명령어로 표현할 수 없습니다. 게시자에게 수정을 요청하세요.", + "download": "다운로드", + "labelsSectionTitle": "라벨", + "labelsSectionDescription": "사용자가 이 스킬을 필터링하고 찾을 수 있도록 추천 라벨을 추가하거나 제거하세요.", + "labelsSectionDescriptionSuperAdmin": "공개 필터에서 숨겨지는 권한 제한 라벨을 포함하여 이 스킬의 모든 라벨을 관리하세요.", + "currentLabelsTitle": "현재 라벨", + "availableLabelsTitle": "사용 가능한 라벨", + "noLabelsAssigned": "아직 이 스킬에 연결된 라벨이 없습니다.", + "loadingAvailableLabels": "사용 가능한 라벨을 불러오는 중...", + "noAvailableLabels": "현재 추가할 수 있는 라벨이 더 없습니다.", + "addLabel": "{{label}} 추가", + "removeLabel": "제거", + "labelRestrictedHint": "최고 관리자 전용", + "labelAttachSuccessTitle": "라벨을 추가했습니다", + "labelAttachSuccessDescription": "스킬 라벨을 업데이트했습니다.", + "labelAttachErrorTitle": "라벨을 추가하지 못했습니다", + "labelDetachSuccessTitle": "라벨을 제거했습니다", + "labelDetachSuccessDescription": "스킬 라벨을 업데이트했습니다.", + "labelDetachErrorTitle": "라벨을 제거하지 못했습니다", + "labelActionFallbackError": "라벨 작업을 완료하지 못했습니다.", + "lifecycle": "수명 주기", + "lifecycleHint": "이 스킬을 보관할 수 있습니다. 보관된 스킬은 일반 사용자에게 숨겨지며 다운로드할 수 없습니다.", + "archivedPublishHint": "이 스킬은 보관되었습니다. 새 버전을 게시하기 전에 복원하세요.", + "archivedInstallHint": "이 스킬은 보관되었으며 공개 다운로드를 사용할 수 없습니다.", + "statusActive": "활성", + "statusArchived": "보관됨", + "statusHidden": "숨김", + "versionStatusDraft": "초안", + "versionStatusScanning": "검사 중", + "versionStatusScanFailed": "검사 실패", + "versionStatusUploaded": "업로드됨", + "versionStatusPendingReview": "검토 대기", + "versionStatusPublished": "게시됨", + "versionStatusRejected": "반려됨", + "versionStatusYanked": "배포 철회됨", + "pendingPreviewBadge": "대기 버전 미리보기", + "rejectedBadge": "검토 반려", + "pendingPreviewTitle": "대기 중인 버전을 미리 보고 있습니다", + "pendingPreviewDescription": "이 버전은 본인에게만 표시됩니다. 검토 승인 전에는 README, 파일, 버전 정보를 확인할 수 있지만 즐겨찾기, 평점, 신고, 다운로드는 사용할 수 없습니다.", + "pendingPreviewInteractionHint": "검토 대기 중인 버전에서는 즐겨찾기, 평점, 신고를 사용할 수 없습니다.", + "rejectedPreviewDescription": "이 버전은 반려되었으며 현재 본인에게만 표시됩니다. 아래 의견을 반영하여 수정한 후 다시 게시하세요.", + "rejectedPreviewInteractionHint": "반려된 미리보기 버전에서는 즐겨찾기, 평점, 신고를 사용할 수 없습니다.", + "rejectedFeedbackTitle": "검토 반려", + "rejectedFeedbackLabel": "검토자 의견", + "rejectedFeedbackFallback": "반려 의견이 제공되지 않았습니다.", + "governance": "거버넌스", + "processing": "처리하는 중...", + "archiveSkill": "스킬 보관", + "unarchiveSkill": "스킬 복원", + "deleteSkill": "스킬 삭제", + "deleteSkillContinue": "계속", + "deleteSkillFinal": "영구 삭제", + "withdrawReview": "검토 철회", + "hideSkill": "스킬 숨기기", + "unhideSkill": "스킬 숨김 해제", + "archiveConfirmTitle": "스킬 보관", + "archiveConfirmDescription": "보관하면 일반 사용자가 \"{{skill}}\"을 보거나 다운로드할 수 없습니다. 계속하시겠습니까?", + "unarchiveConfirmTitle": "스킬 복원", + "unarchiveConfirmDescription": "\"{{skill}}\"을 복원하면 다시 표시되며 새 버전을 게시할 수 있습니다.", + "deleteSkillConfirmTitle": "스킬 영구 삭제", + "deleteSkillConfirmDescription": "\"{{skill}}\"의 모든 버전, 파일, 다운로드 번들을 삭제합니다. 이 작업은 되돌릴 수 없습니다.", + "deleteSkillInputTitle": "확인하려면 스킬 슬러그를 입력하세요", + "deleteSkillInputDescription": "계속하려면 스킬 슬러그 \"{{slug}}\"를 입력하세요.", + "deleteSkillInputPlaceholder": "스킬 슬러그 입력", + "deleteSkillWarning": "물리적으로 완전히 삭제합니다. 이전 버전과 번들이 영구적으로 제거됩니다.", + "archiveSuccessTitle": "스킬을 보관했습니다", + "archiveSuccessDescription": "\"{{skill}}\"을 보관했습니다.", + "archiveErrorTitle": "스킬을 보관하지 못했습니다", + "unarchiveSuccessTitle": "스킬을 복원했습니다", + "unarchiveSuccessDescription": "\"{{skill}}\"을 복원했습니다.", + "unarchiveErrorTitle": "스킬을 복원하지 못했습니다", + "deleteSkillSuccessTitle": "스킬을 삭제했습니다", + "deleteSkillSuccessDescription": "\"{{skill}}\"을 영구 삭제했습니다.", + "deleteSkillErrorTitle": "스킬을 삭제하지 못했습니다", + "withdrawReviewConfirmTitle": "검토 철회", + "withdrawReviewConfirmDescription": "철회하면 {{version}} 버전이 검토 대기열에서 빠지고 초안으로 돌아가 나중에 다시 제출할 수 있습니다.", + "withdrawReviewSuccessTitle": "검토를 철회했습니다", + "withdrawReviewSuccessDescription": "{{version}} 버전의 검토를 철회했습니다.", + "withdrawReviewErrorTitle": "검토를 철회하지 못했습니다", + "confirmPublish": "게시 확인", + "confirmPublishDialogTitle": "게시 확인", + "confirmPublishDialogDescription": "{{version}} 버전을 비공개 스킬로 게시하시겠습니까? 본인은 다운로드하고 설치할 수 있지만 마켓플레이스에는 표시되지 않습니다.", + "confirmPublishSuccessTitle": "버전을 게시했습니다", + "confirmPublishSuccessDescription": "{{version}} 버전을 비공개 스킬로 게시했습니다.", + "confirmPublishErrorTitle": "게시를 확인하지 못했습니다", + "submitReview": "검토 요청", + "submitReviewDialogTitle": "검토 요청", + "submitReviewDialogDescription": "{{version}} 버전을 공개 검토에 제출하시겠습니까? 승인되면 마켓플레이스에 표시됩니다.", + "submitReviewSuccessTitle": "검토를 요청했습니다", + "submitReviewSuccessDescription": "{{version}} 버전의 검토를 요청했습니다.", + "submitReviewErrorTitle": "검토를 요청하지 못했습니다", + "deleteVersion": "버전 삭제", + "deleteVersionConfirmTitle": "버전 삭제", + "deleteVersionConfirmDescription": "{{version}} 버전을 삭제하면 복구할 수 없습니다. 계속하시겠습니까?", + "deleteVersionSuccessTitle": "버전을 삭제했습니다", + "deleteVersionSuccessDescription": "{{version}} 버전을 삭제했습니다.", + "deleteVersionErrorTitle": "버전을 삭제하지 못했습니다", + "currentVersion": "현재", + "currentPublicVersion": "현재 공개 버전", + "pendingReviewSectionTitle": "검토 대기 버전", + "pendingReviewSectionDescription": "v{{pendingVersion}}은 현재 검토 중입니다. 공개 및 설치 가능한 버전은 계속 v{{publishedVersion}}입니다.", + "pendingReviewVersionLabel": "대기 버전", + "pendingReviewStatusLabel": "검토 상태", + "pendingReviewStatusValue": "검토 중", + "lifecyclePublicVersionLabel": "현재 공개 버전", + "lifecycleNoPublishedVersion": "게시된 버전 없음", + "lifecyclePendingVersionLabel": "검토 대기 버전", + "lifecyclePendingVersionValue": "v{{version}} 검토 중", + "lifecycleNoPendingVersion": "검토 대기 버전 없음", + "lifecycleContainerStateLabel": "스킬 컨테이너 상태", + "compareVersions": "비교", + "compareDialogTitle": "버전 비교", + "compareDialogDescription": "v{{source}}와 v{{target}}을 비교합니다.", + "compareSourceLabel": "선택한 버전", + "compareTargetLabel": "비교 대상", + "versionCompareUnavailableTitle": "비교할 버전이 부족합니다", + "versionCompareUnavailableDescription": "버전을 비교하려면 두 개 이상의 버전을 게시하세요.", + "metadataChanges": "메타데이터 변경", + "noMetadataChanges": "메타데이터 변경 없음", + "readmeChange": "README 변경", + "readmeChanged": "README 내용 변경됨", + "readmeUnchanged": "README 내용 변경 없음", + "fileChanges": "파일 변경", + "filesAdded": "추가됨", + "filesRemoved": "제거됨", + "filesChanged": "변경됨", + "expandDiff": "차이 보기", + "collapseDiff": "차이 숨기기", + "binaryFileNotice": "바이너리 파일: 차이를 표시할 수 없습니다", + "largeFileWarning": "큰 파일이므로 화면 표시가 느릴 수 있습니다.", + "diffLoadError": "파일 내용을 불러오지 못했습니다", + "diffRetry": "다시 시도", + "diffViewUnified": "통합 보기", + "diffViewSplit": "나란히 보기", + "rereleaseVersion": "재릴리스", + "rereleaseDialogTitle": "기존 버전으로 재릴리스", + "rereleaseDialogDescription": "v{{version}}을 기반으로 새 게시 버전을 만듭니다.", + "rereleaseSourceVersion": "원본 버전", + "rereleaseTargetVersion": "새 버전", + "rereleaseSuccessTitle": "버전을 재릴리스했습니다", + "rereleaseSuccessDescription": "v{{source}}를 기반으로 v{{target}}을 만들었습니다.", + "rereleaseErrorTitle": "버전을 재릴리스하지 못했습니다", + "rereleaseWarningTitle": "게시 전 경고", + "rereleaseWarningDescription": "다음 위험 안내를 확인했습니다. 내용을 이해하고 진행하려면 재릴리스를 계속할 수 있습니다.", + "rereleaseWarningConfirm": "재릴리스 계속", + "yankVersion": "현재 버전 배포 철회", + "promoteToGlobal": "글로벌로 승격", + "promotionSectionTitle": "글로벌로 승격", + "promotionSectionDescription": "현재 게시된 v{{version}}을 글로벌 네임스페이스 검토에 제출합니다.", + "promotionSourceVersion": "게시된 팀 버전 선택", + "promotionStateLoading": "글로벌 게시 상태를 불러오는 중…", + "promotionStateError": "글로벌 게시 상태를 불러오지 못했습니다. 다시 시도하세요.", + "promotionNoGlobalSkill": "아직 게시된 글로벌 스킬이 없습니다.", + "promotionNoGlobalVersion": "없음", + "promotionGlobalCurrent": "현재 최신 글로벌 버전: v{{version}}", + "promotionPending": "플랫폼 검토 대기 중인 요청이 있습니다. 처리된 후 다시 제출할 수 있습니다.", + "submitGlobalUpdate": "이 버전을 글로벌 검토에 제출", + "globalUpdateConfirmTitle": "글로벌 스킬 업데이트 확인", + "globalUpdateConfirmDescription": "“{{skill}}”의 팀 버전 v{{source}}를 글로벌 검토에 제출합니다. 현재 최신 글로벌 버전은 v{{target}}이며 검토 중에 변경될 수 있습니다. 버전 충돌 없이 승인되면 버전 번호가 더 낮아도 이 버전이 최신 게시 버전이 됩니다.", + "promotionVersionConflictTitle": "글로벌 버전이 이미 존재합니다", + "promotionVersionConflictDescription": "글로벌 스킬에 이 버전 번호가 이미 존재합니다. 다른 게시된 팀 버전을 선택하세요.", + "revocationDescription": "승격을 취소하면 별도로 업로드한 버전을 포함하여 연결된 글로벌 스킬 전체가 삭제됩니다. 팀 스킬은 유지됩니다.", + "requestRevocation": "글로벌 승격 취소 요청", + "directRevocation": "관리자로 즉시 승격 취소", + "revocationConfirmTitle": "글로벌 승격 취소 확인", + "revocationConfirmDescription": "“{{skill}}”의 @global/{{slug}}를 모든 글로벌 버전, 평점, 통계와 함께 삭제합니다. 팀 스킬은 유지됩니다. 설치된 로컬 파일은 회수할 수 없으며 이후 다른 스킬이 이 주소를 사용할 수 있습니다.", + "revocationReasonLabel": "승격 취소 사유", + "revocationReasonPlaceholder": "사유(선택 사항)", + "revocationPending": "승격 취소 요청이 플랫폼 검토 대기 중입니다.", + "revocationRequestSuccess": "승격 취소 요청을 제출했습니다", + "revocationDirectSuccess": "글로벌 스킬의 승격을 취소했습니다", + "revocationError": "승격 취소 실패", + "revocationHistoryTitle": "승격 취소 내역", + "revocationHistoryItem": "{{date}} · {{status}}", + "revocationHistoryLoading": "승격 취소 내역을 불러오는 중…", + "revocationHistoryError": "승격 취소 내역을 불러오지 못했습니다. 다시 시도하세요.", + "revocationStatus": { + "PENDING": "대기 중", + "APPROVED": "취소됨", + "REJECTED": "반려됨" + }, + "promotionConfirmTitle": "승격 요청 제출", + "promotionConfirmDescription": "\"{{skill}}\"의 v{{version}}을 글로벌 네임스페이스로 승격 요청하시겠습니까?", + "promotionSuccessTitle": "승격 요청을 제출했습니다", + "promotionSuccessDescription": "\"{{skill}}\"의 v{{version}}이 글로벌 승격 검토 대기열에 등록되었습니다.", + "promotionDuplicateTitle": "이미 승격 대기 중", + "promotionDuplicateDescription": "이 버전에는 이미 대기 중인 승격 요청이 있습니다.", + "promotionAlreadyPromotedTitle": "이미 승격된 스킬", + "promotionAlreadyPromotedDescription": "이 스킬은 이미 글로벌 네임스페이스로 승격되었습니다.", + "promotionErrorTitle": "승격 요청을 제출하지 못했습니다", + "reportSkill": "스킬 신고", + "reportedSkill": "신고됨", + "reportDialogTitle": "스킬 신고", + "reportDialogDescription": "관리자가 신속하게 검토하고 조치할 수 있도록 사유를 입력하세요.", + "reportReasonPlaceholder": "정책 위반, 오해의 소지가 있는 콘텐츠, 권리 침해 등의 사유", + "reportDetailsPlaceholder": "추가 설명(선택 사항)", + "submitReport": "신고 제출", + "reportReasonRequired": "신고 사유를 입력하세요", + "reportSuccessTitle": "신고를 제출했습니다", + "reportSuccessDescription": "관리자가 곧 신고를 검토합니다.", + "downloadErrorTitle": "다운로드 실패", + "reportErrorTitle": "신고 실패", + "share": { + "button": "공유", + "copied": "복사됨", + "defaultDescription": "유용한 스킬" + }, + "installForAgent": { + "button": "에이전트용 설치", + "copied": "에이전트 프롬프트를 복사했습니다", + "prompt": "{{guideUrl}}의 안내에 따라 SkillHub에 연결하고 이 SkillHub에서 {{skill}}의 {{version}} 버전을 설치하세요." + } + }, + "skillCompare": { + "pageTitle": "버전 비교", + "filesChanged": "파일 {{count}}개 변경됨", + "searchFiles": "파일 검색", + "errorLoadingCompare": "버전 비교를 불러오지 못했습니다", + "loadDiff": "차이 불러오기", + "loading": "불러오는 중...", + "totalFiles": "파일", + "addedLines": "추가된 줄", + "removedLines": "삭제된 줄", + "baseVersion": "기준 버전", + "headVersion": "비교 버전", + "fileList": "파일 목록", + "binaryFile": "바이너리 파일: 차이를 표시할 수 없습니다", + "truncatedFile": "차이 출력이 잘렸습니다", + "notEnoughPublishedVersions": "비교하려면 두 개 이상의 버전을 게시하세요.", + "noFilesFound": "일치하는 파일이 없습니다.", + "changeTypeAdded": "추가됨", + "changeTypeModified": "수정됨", + "changeTypeRemoved": "제거됨" + }, + "reports": { + "title": "스킬 신고", + "subtitle": "사용자가 제출한 스킬 신고를 처리하세요", + "tabPending": "대기 중", + "tabResolved": "처리됨", + "tabDismissed": "기각됨", + "empty": "신고 없음", + "reporter": "신고자", + "handledBy": "처리자", + "resolve": "처리", + "resolveAndHide": "처리 및 숨김", + "resolveAndArchive": "처리 및 보관", + "dismiss": "기각", + "resolveConfirmTitle": "신고 처리", + "resolveConfirmDescription": "\"{{skill}}\"에 대한 신고를 처리 완료로 표시하시겠습니까?", + "resolveAndHideConfirmDescription": "\"{{skill}}\"에 대한 신고를 처리하고 공개 접근에서 스킬을 숨기시겠습니까?", + "resolveAndArchiveConfirmDescription": "\"{{skill}}\"에 대한 신고를 처리하고 스킬을 보관하시겠습니까?", + "dismissConfirmTitle": "신고 기각", + "dismissConfirmDescription": "\"{{skill}}\"에 대한 신고를 기각하시겠습니까?", + "resolveSuccessTitle": "신고를 처리했습니다", + "resolveSuccessDescription": "\"{{skill}}\"에 대한 신고를 처리 완료로 표시했습니다.", + "resolveAndHideSuccessTitle": "신고를 처리하고 스킬을 숨겼습니다", + "resolveAndHideSuccessDescription": "신고 처리 후 \"{{skill}}\"을 숨겼습니다.", + "resolveAndArchiveSuccessTitle": "신고를 처리하고 스킬을 보관했습니다", + "resolveAndArchiveSuccessDescription": "신고 처리 후 \"{{skill}}\"을 보관했습니다.", + "dismissSuccessTitle": "신고를 기각했습니다", + "dismissSuccessDescription": "\"{{skill}}\"에 대한 신고를 기각했습니다.", + "resolveErrorTitle": "신고를 처리하지 못했습니다", + "resolveAndHideErrorTitle": "신고 처리 및 스킬 숨김에 실패했습니다", + "resolveAndArchiveErrorTitle": "신고 처리 및 스킬 보관에 실패했습니다", + "dismissErrorTitle": "신고를 기각하지 못했습니다" + }, + "governance": { + "title": "거버넌스 센터", + "subtitle": "검토, 승격, 신고, 감사 활동을 한곳에서 확인하세요.", + "pendingReviews": "검토 대기", + "pendingPromotions": "승격 대기", + "pendingReports": "신고 대기", + "unreadNotifications": "읽지 않은 알림", + "inboxTitle": "거버넌스 수신함", + "inboxSubtitle": "조치가 필요한 운영 작업을 모은 대기열입니다.", + "tabAll": "전체", + "tabReview": "검토", + "tabPromotion": "승격", + "tabReport": "신고", + "emptyInbox": "현재 처리할 거버넌스 작업이 없습니다.", + "openItem": "열림", + "notificationsTitle": "알림", + "notificationsSubtitle": "확인이 필요한 최근 거버넌스 업데이트입니다.", + "emptyNotifications": "아직 알림이 없습니다.", + "markRead": "읽음으로 표시", + "activityTitle": "거버넌스 활동", + "activitySubtitle": "검토, 승격, 신고, 수명 주기 작업에 대한 최근 감사 이벤트입니다.", + "emptyActivity": "최근 거버넌스 활동이 없습니다.", + "unknownActor": "알 수 없는 작업자", + "searchMaintenanceTitle": "검색 인덱스 유지보수", + "searchMaintenanceDescription": "전체 스킬 검색 인덱스를 재구축합니다. 최고 관리자만 실행할 수 있으며 검색 규칙 변경 후 전체 데이터를 다시 반영할 때 사용합니다.", + "searchMaintenanceHint": "시간이 걸릴 수 있습니다. 반복해서 실행하지 마세요.", + "searchRebuildAction": "전체 검색 인덱스 재구축", + "searchRebuildRunning": "재구축하는 중...", + "searchRebuildConfirmTitle": "전체 검색 인덱스를 재구축하시겠습니까?", + "searchRebuildConfirmDescription": "현재 인덱싱 규칙으로 모든 스킬의 검색 문서를 재구축합니다. 시간이 걸릴 수 있습니다.", + "searchRebuildSuccessTitle": "검색 인덱스 재구축을 시작했습니다", + "searchRebuildSuccessDescription": "현재 규칙으로 전체 검색 인덱스를 재구축하고 있습니다.", + "searchRebuildErrorTitle": "검색 인덱스 재구축 실패" + }, + "invitation": { + "title": "팀 공간 참여", + "create": "멤버 초대", + "revoke": "초대 취소", + "replaceTitle": "현재 초대를 교체하시겠습니까?", + "replaceDescription": "기존 링크는 즉시 사용할 수 없게 됩니다. 기존 멤버에는 영향을 주지 않습니다.", + "revokeTitle": "현재 초대를 취소하시겠습니까?", + "revokeDescription": "링크는 즉시 사용할 수 없게 됩니다. 기존 멤버에는 영향을 주지 않습니다.", + "link": "초대 링크", + "linkHint": "24시간 동안 새 멤버 한 명이 사용할 수 있습니다. 안전하게 공유하세요.", + "copied": "초대 링크를 복사했습니다", + "replacedCopied": "기존 초대를 교체하고 새 링크를 복사했습니다", + "manualCopy": "자동 복사에 실패했습니다. 아래 링크를 직접 복사하세요.", + "revoked": "초대를 취소했습니다", + "error": "초대 작업에 실패했습니다. 다시 시도하세요.", + "invalid": "초대 링크가 유효하지 않거나 만료되었습니다. 공간 관리자에게 새 링크를 요청하세요.", + "loading": "초대를 확인하는 중…", + "networkError": "연결에 실패했습니다. 다시 시도하세요.", + "namespace": "공간: {{name}}", + "account": "현재 계정: {{name}}", + "permissions": "확인하면 멤버(MEMBER)로 참여합니다. 공간 리소스에 접근하고 스킬을 게시할 수 있습니다.", + "alreadyMember": "이미 이 공간의 멤버입니다.", + "enter": "공간으로 이동", + "confirm": "확인 및 참여", + "activeUnavailable": "초대가 활성화되어 있지만 이 탭에는 저장된 링크가 없습니다. 링크를 만든 탭에서 복사하거나, 기존 링크를 무효화하고 새 링크를 만들도록 확인하세요.", + "localLinkHint": "링크는 이 브라우저 탭의 현재 계정에만 저장되며, 24시간 이내에 새 멤버 한 명이 사용할 수 있습니다.", + "copy": "링크 복사", + "storageUnavailable": "브라우저에 링크를 저장하지 못했습니다. 지금 복사하세요. 이 창을 닫거나 새로고침하면 복원할 수 없습니다.", + "statusLoading": "초대 상태를 확인하는 중", + "statusActive": "초대 활성", + "statusInactive": "활성 초대 없음", + "detailsTitle": "참여 안내", + "singleUse": "24시간 이내에 새 멤버 한 명이 이 초대를 사용할 수 있습니다.", + "memberAccess": "초대받은 사용자는 멤버로 참여하며 이 공간에 공유된 스킬에 접근할 수 있습니다.", + "accessPolicy": "초대받은 사용자는 로그인하고 기존 가입 규칙을 충족해야 합니다. 링크를 열기만 해서는 자동으로 참여되지 않습니다.", + "replaceAction": "초대 링크 교체", + "managementTitle": "초대 관리", + "retry": "상태 새로고침", + "expiresAtLabel": "만료 시각", + "statusError": "초대 상태를 확인할 수 없습니다", + "historyTitle": "초대 내역", + "historyDescription": "링크 생성, 변경, 참여 성공 기록입니다. 내역이 현재 멤버 자격을 의미하지는 않습니다.", + "historyLoading": "초대 내역을 불러오는 중", + "historyEmpty": "초대 기록 없음", + "historyError": "초대 내역을 불러오지 못했습니다. 다시 시도하세요.", + "historyCreator": "생성자", + "historyCreatedAt": "생성 시각", + "historyExpiresAt": "만료 시각", + "historyStatus": "상태", + "historyJoined": "참여자 / 참여 시각", + "historyLegacy": "이전 기록", + "historyUnknownUser": "이름이 기록되지 않음", + "historyNoJoin": "기록된 참여 없음", + "historyActive": "활성", + "historyUsed": "사용됨", + "historyReplaced": "교체됨", + "historyRevoked": "취소됨", + "historyExpired": "만료됨", + "historyRefresh": "내역 새로고침" + }, + "members": { + "title": "멤버 관리", + "addMember": "멤버 추가", + "addingMember": "추가하는 중...", + "addDialogTitle": "네임스페이스 멤버 추가", + "addDialogDescription": "플랫폼 사용자를 검색하거나 사용자 ID를 직접 입력하세요. 추가된 사용자는 이 네임스페이스에서 즉시 협업할 수 있습니다.", + "searchLabel": "사용자 검색", + "searchPlaceholder": "사용자 이름, 이메일 또는 사용자 ID로 검색", + "searchHint": "검색하려면 2자 이상 입력하세요. 검색 결과에는 직접 추가할 수 있는 사용자 ID가 포함됩니다.", + "searchAction": "검색", + "searchTooShort": "검색어는 2자 이상이어야 합니다", + "searchResultsTitle": "후보 사용자", + "searchEmpty": "추가할 수 있는 활성 사용자가 없습니다", + "selectCandidate": "이 사용자 선택", + "manualUserIdLabel": "사용자 ID 직접 입력", + "manualUserIdPlaceholder": "예: user-123 또는 usr_abcd", + "manualUserIdHint": "사용자 ID를 알고 있다면 여기에서 바로 추가할 수 있습니다.", + "userIdRequired": "사용자 ID를 입력하세요", + "roleLabel": "멤버 역할", + "roleOwner": "소유자", + "roleAdmin": "관리자", + "roleMember": "멤버", + "saveRole": "역할 저장", + "savingRole": "저장하는 중...", + "changeRole": "역할 변경", + "colUserId": "사용자 ID", + "colUsername": "사용자 이름", + "colEmail": "이메일", + "colRole": "역할", + "colJoinedAt": "참여 시각", + "colActions": "작업", + "remove": "제거", + "empty": "멤버 없음", + "namespaceNotFound": "네임스페이스를 찾을 수 없습니다", + "globalReadOnly": "기본 제공 시스템 네임스페이스입니다. 멤버 정보를 조회할 수만 있으며 변경할 수 없습니다.", + "frozenReadOnly": "이 네임스페이스는 동결되었습니다. 멤버를 조회할 수 있지만 추가, 제거, 역할 변경은 할 수 없습니다.", + "archivedReadOnly": "이 네임스페이스는 보관되었습니다. 멤버를 조회할 수 있지만 복원하기 전까지 멤버 정보를 변경할 수 없습니다.", + "memberReadOnly": "이 네임스페이스의 일반 멤버입니다. 멤버 목록을 볼 수 있지만 소유자 또는 관리자만 변경할 수 있습니다.", + "addSuccessTitle": "멤버를 추가했습니다", + "addSuccessDescription": "사용자 {{userId}}를 이 네임스페이스에 추가했습니다.", + "addErrorTitle": "멤버를 추가하지 못했습니다", + "updateRoleSuccessTitle": "멤버 역할을 변경했습니다", + "updateRoleSuccessDescription": "사용자 {{userId}}의 역할이 {{role}}(으)로 변경되었습니다.", + "updateRoleErrorTitle": "멤버 역할을 변경하지 못했습니다", + "removeConfirmTitle": "멤버 제거", + "removeConfirmDescription": "제거하면 사용자 {{userId}}는 이 네임스페이스에 접근할 수 없습니다.", + "removeSuccessTitle": "멤버를 제거했습니다", + "removeSuccessDescription": "사용자 {{userId}}를 이 네임스페이스에서 제거했습니다.", + "removeErrorTitle": "멤버를 제거하지 못했습니다", + "searchEmptyMemberHint": "이미 이 네임스페이스에 속한 사용자는 여기에 표시되지 않습니다. 멤버 목록에서 확인하세요.", + "transferOwnership": "소유권 이전", + "transferDialogTitle": "네임스페이스 소유권 이전", + "transferDialogDescription": "다른 멤버에게 소유권을 이전합니다. 본인은 관리자가 됩니다.", + "transferWarning": "이 작업은 되돌릴 수 없습니다. 이전 후에는 더 이상 소유자가 아닙니다.", + "transferNewOwnerLabel": "새 소유자", + "transferSelectPlaceholder": "멤버 선택", + "transferNoCandidates": "소유권을 이전할 다른 멤버가 없습니다.", + "transferConfirmSlugPrompt": "확인하려면 네임스페이스 슬러그를 입력하세요", + "transferConfirmSlugHint": "확인하려면 {{slug}}를 입력하세요", + "transferConfirmAction": "소유권 이전", + "transferring": "이전하는 중...", + "transferSuccessTitle": "소유권을 이전했습니다", + "transferSuccessDescription": "{{userId}}가 이 네임스페이스의 소유자가 되었습니다.", + "transferErrorTitle": "소유권을 이전하지 못했습니다", + "batchImport": "일괄 가져오기", + "batchDialogTitle": "멤버 일괄 가져오기", + "batchDialogDescription": "CSV 파일을 업로드하여 여러 멤버를 한 번에 추가하세요. 각 행에 사용자 ID와 역할을 입력해야 합니다.", + "batchStepUpload": "CSV 업로드", + "batchStepPreview": "미리보기", + "batchStepResults": "결과", + "batchDownloadTemplate": "CSV 템플릿 다운로드", + "batchDropHint": "CSV 파일을 여기로 끌어 놓거나 클릭하여 선택하세요", + "batchFormatHint": ".csv 형식만 지원합니다", + "batchParseError": "CSV 파일을 해석하지 못했습니다", + "batchEmptyFile": "CSV 파일이 비어 있거나 유효한 행이 없습니다", + "batchPreviewTitle": "미리보기({{count}}행)", + "batchColUserId": "사용자 ID", + "batchColRole": "역할", + "batchColStatus": "상태", + "batchValidationMissingUserId": "사용자 ID 누락", + "batchValidationInvalidRole": "올바르지 않은 역할(MEMBER 또는 ADMIN이어야 함)", + "batchValidationDuplicate": "중복 사용자 ID", + "batchValidationValid": "준비됨", + "batchValidRows": "유효 {{valid}}개, 무효 {{invalid}}개", + "batchSubmitting": "가져오는 중...", + "batchSubmit": "멤버 {{count}}명 가져오기", + "batchResultTitle": "가져오기 결과", + "batchResultSummary": "총 {{total}}개 중 성공 {{success}}개, 실패 {{failure}}개", + "batchResultSuccess": "추가됨", + "batchResultAlreadyMember": "이미 멤버입니다", + "batchResultUserNotFound": "사용자를 찾을 수 없습니다", + "batchResultInvalidRole": "올바르지 않은 역할", + "batchResultUnknownError": "알 수 없는 오류", + "batchDone": "완료", + "batchBack": "뒤로", + "cancelRole": "역할 변경 취소" + }, + "namespaceEdit": { + "editButton": "편집", + "dialogTitle": "네임스페이스 편집", + "displayNameLabel": "표시 이름", + "displayNameRequired": "표시 이름을 입력하세요", + "descriptionLabel": "설명", + "saveAction": "저장", + "saving": "저장하는 중...", + "saveSuccess": "네임스페이스를 업데이트했습니다", + "saveErrorTitle": "네임스페이스를 업데이트하지 못했습니다" + }, + "upload": { + "dropHint": "놓아서 업로드하세요...", + "dragHint": "ZIP 파일을 여기로 끌어 놓거나 클릭하여 선택하세요", + "formatHint": ".zip 형식만 지원합니다", + "folderHint": "또는 폴더를 선택하여 패키지로 묶어 업로드하세요" + }, + "layout": { + "openNavigation": "탐색 메뉴 열기", + "closeNavigation": "탐색 메뉴 닫기", + "footerDescription": "개발자를 위한 효율적인 스킬 관리와 배포를 제공하는 스킬 레지스트리입니다." + }, + "nsReviews": { + "title": "네임스페이스 검토", + "loadingNamespace": "네임스페이스 정보를 불러오는 중", + "reviewsFor": "{{name}}의 검토 작업", + "empty": "검토 기록 없음", + "version": "버전 {{version}}", + "tabPending": "대기 중", + "tabApproved": "승인됨", + "tabRejected": "반려됨", + "sortLabel": "시간순 정렬", + "sortNewest": "최신순", + "sortOldest": "오래된 순", + "openReview": "검토 열기", + "pageSummary": "총 {{total}}개 기록, {{page}}페이지", + "prevPage": "이전", + "nextPage": "다음", + "globalReadOnly": "기본 제공 시스템 네임스페이스입니다. 검토 내역을 볼 수 있지만 새로운 검토 작업은 수행할 수 없습니다.", + "frozenReadOnly": "이 네임스페이스는 동결되었습니다. 검토 내역을 볼 수 있지만 검토 작업을 계속 처리할 수 없습니다.", + "archivedReadOnly": "이 네임스페이스는 보관되었습니다. 검토 내역을 볼 수 있지만 복원하기 전까지 검토 작업을 처리할 수 없습니다.", + "statusLabel": "검토 상태" + }, + "namespaceStatus": { + "active": "활성", + "frozen": "동결됨", + "archived": "보관됨", + "frozenHint": "이 네임스페이스는 현재 읽기 전용이므로 게시, 검토, 멤버 변경을 할 수 없습니다.", + "archivedHint": "이 네임스페이스는 공개 진입점에서 숨겨져 있으며 대시보드의 멤버에게만 표시됩니다.", + "immutableHint": "기본 제공 시스템 네임스페이스이며 거버넌스나 멤버 변경을 허용하지 않습니다." + }, + "fileTree": { + "title": "파일" + }, + "filePreview": { + "loadError": "파일을 불러오지 못했습니다", + "tooLarge": "파일이 너무 커서 미리 볼 수 없습니다", + "binaryFile": "바이너리 파일은 미리 볼 수 없습니다", + "unsupported": "미리보기를 지원하지 않는 파일 유형입니다", + "downloadFile": "파일 다운로드", + "downloadHint": "{{name}} 다운로드", + "copy": "내용 복사", + "copySuccess": "클립보드에 복사했습니다", + "close": "닫기" + }, + "loginButton": { + "loading": "불러오는 중...", + "loadingShort": "불러오는 중", + "loginWith": "{{name}}으로 로그인" + }, + "authShell": { + "eyebrow": "SkillHub", + "backHome": "홈으로 돌아가기", + "heroTitle": "팀의 스킬을 발견하고\n보존하고 재사용하세요", + "heroSubtitle": "SKILL.md로 시작하세요.\n경험을 팀이 공유할 수 있는 역량으로 만드세요.", + "heroFooter": "팀의 지식을 모아 스킬의 가치를 높이세요", + "shortTagline": "스킬로\n함께\n만들어요", + "capabilities": "SkillHub로 할 수 있는 일", + "discoverTitle": "스킬 탐색", + "discoverDescription": "팀에서 재사용할 수 있는 스킬과 실무 사례를 찾으세요.", + "shareTitle": "지식 공유", + "shareDescription": "버전을 게시하고 경험을 나누세요.", + "governTitle": "안전한 관리", + "governDescription": "명확한 권한으로 협업하고 접근을 관리하세요.", + "previewNavLibrary": "라이브러리", + "previewNavMine": "내 스킬", + "previewNavTeam": "팀 공간", + "previewSearch": "스킬, 키워드 또는 팀 검색", + "previewChipAll": "전체", + "previewChipProduct": "제품", + "previewChipData": "데이터", + "previewChipCommon": "일반", + "previewRowProduct": "제품", + "previewRowData": "데이터", + "previewRowDev": "개발", + "previewRowCommon": "일반" + }, + "ratingInput": { + "yourRating": "내 평점: {{score}}점" + }, + "skillReviews": { + "title": "사용자 리뷰", + "count": "리뷰 {{count}}개", + "count_other": "리뷰 {{count}}개", + "write": "리뷰 작성", + "edit": "내 리뷰 편집", + "scoreLabel": "내 점수", + "ratingDisplay": "평점: 5점 만점에 {{score}}점", + "ratingOption": "5점 만점에 {{score}}점 주기", + "reviewTextLabel": "리뷰 내용", + "placeholder": "유용했던 점과 다른 사용자가 알아야 할 내용을 공유하세요.", + "save": "리뷰 저장", + "cancel": "취소", + "delete": "리뷰 삭제", + "takedown": "아바타 내리기", + "takedownHelp": "현재 아바타를 제거할 사유를 입력하세요. 스킬의 첫 글자가 대신 표시됩니다.", + "saved": "리뷰를 저장했습니다", + "deleted": "리뷰를 삭제했습니다. 별점은 유지됩니다", + "textRequired": "저장하기 전에 리뷰를 입력하세요", + "saveFailed": "리뷰를 저장하지 못했습니다", + "deleteFailed": "리뷰를 삭제하지 못했습니다", + "loadFailed": "리뷰를 불러오지 못했습니다. 나중에 다시 시도하세요.", + "empty": "아직 리뷰가 없습니다. 첫 번째로 경험을 공유해 보세요.", + "loading": "리뷰를 불러오는 중...", + "hiddenStatus": "숨김", + "yourReviewHidden": "내 리뷰가 공개 목록에서 숨겨져 있습니다.", + "moderationReason": "사유: {{reason}}", + "hide": "리뷰 숨기기", + "restore": "리뷰 복원", + "hidden": "리뷰를 숨겼습니다", + "restored": "리뷰를 복원했습니다", + "moderationFailed": "리뷰 공개 상태를 변경하지 못했습니다", + "previous": "이전", + "next": "다음" + }, + "createToken": { + "title": "API 토큰 만들기", + "description": "CLI 또는 API 접근을 위한 새 API 토큰을 만드세요", + "nameLabel": "토큰 이름", + "namePlaceholder": "예: my-cli-token", + "nameRequired": "토큰 이름을 입력하세요", + "nameTooLong": "토큰 이름은 {{max}}자 이하여야 합니다", + "nameDuplicate": "같은 이름의 토큰이 이미 있습니다", + "creating": "생성하는 중...", + "create": "만들기", + "successTitle": "토큰을 만들었습니다", + "successDescription": "지금 토큰을 복사하여 저장하세요. 생성할 때만 표시하고 복사할 수 있습니다. 나중에 다시 필요하면 새 토큰을 만드세요.", + "tokenLabel": "토큰", + "nameDisplay": "이름", + "expirationLabel": "만료", + "expirationHint": "기본적으로 토큰은 만료되지 않습니다. 자동 만료 시각을 설정할 수도 있습니다.", + "expirationNever": "만료 없음", + "expiration7d": "7일 후 만료", + "expiration30d": "30일 후 만료", + "expiration90d": "90일 후 만료", + "expirationCustom": "날짜 및 시각 직접 지정", + "expiresAtRequired": "만료 시각을 선택하세요", + "expiresAtDisplay": "만료 시각", + "copyToken": "토큰 복사", + "copySuccess": "토큰을 클립보드에 복사했습니다", + "copyFailed": "토큰을 복사하지 못했습니다. 다시 시도하세요." + }, + "starButton": { + "starred": "즐겨찾기됨", + "star": "즐겨찾기" + }, + "subscribeButton": { + "subscribed": "구독 중", + "subscribe": "구독" + }, + "skillCard": { + "downloads": "다운로드 {{value}}회", + "stars": "즐겨찾기 {{count}}개", + "rating": "평점 {{rating}}", + "starred": "즐겨찾기됨", + "starredAction": "클릭하여 즐겨찾기 해제", + "unstarTitle": "즐겨찾기에서 제거", + "unstarDescription": "\"{{name}}\"을 즐겨찾기한 스킬에서 제거하시겠습니까?", + "unstarConfirm": "제거" + }, + "copyButton": { + "copied": "복사됨", + "copy": "복사" + }, + "pagination": { + "label": "페이지 탐색", + "prev": "이전", + "next": "다음", + "pagePrefix": "페이지", + "pageSuffix": "", + "goToPage": "{{page}}페이지로 이동" + }, + "systemConfig": { + "intro": "로그인 방식과 새 외부 계정 생성 시 부여할 역할을 관리하세요.", + "passwordLoginHelp": "비활성화하면 로컬 계정으로 비밀번호 로그인을 다시 할 수 없습니다. 기존 세션은 만료될 때까지 유지됩니다.", + "selfRegistrationHelp": "비활성화하면 방문자가 로컬 계정을 만들 수 없습니다. 기존 계정에는 영향을 주지 않습니다.", + "unsaved": "저장하지 않은 변경 사항", + "changedElsewhere": "다른 관리자가 이 설정을 변경했습니다. 업데이트된 값을 확인한 후 다시 저장하세요.", + "discard": "변경 사항 버리기", + "saveChanges": "변경 사항 저장", + "saving": "저장하는 중…", + "loading": "불러오는 중…", + "lockoutTitle": "로컬 비밀번호 로그인을 비활성화하시겠습니까?", + "confirmDisableLogin": "로그인 비활성화", + "ruleIntro": "접근 검사를 통과한 새 외부 계정에 부여할 플랫폼 역할을 선택하세요.", + "ruleApplies": "접근 검사 후 새로 생성된 외부 계정에만 적용됩니다. 기존 사용자의 역할은 사용자 관리에서 부여하세요.", + "ruleNoMerge": "이메일이 같아도 로컬 계정과 외부 계정은 병합되지 않습니다. 역할은 새 외부 계정에만 부여됩니다.", + "ruleUnavailable": "Feishu와 DingTalk는 현재 검증된 이메일을 제공하지 않으므로 이 규칙을 사용할 수 없습니다.", + "providerExample": "예: github", + "existingRules": "기존 규칙", + "noRules": "아직 규칙이 없습니다. 위 양식에서 추가하세요.", + "disableTitle": "이 역할 규칙을 비활성화하시겠습니까?", + "title": "시스템 설정", + "localAuth": "로컬 계정", + "lockoutHelp": "비밀번호 로그인을 비활성화하기 전에 외부 로그인과 최고 관리자 접근이 정상 동작하는지 확인하세요.", + "grantedTo": "계정 {{userId}}에 부여됨(외부 식별자 {{subject}})", + "passwordLogin": "로컬 비밀번호 로그인 허용", + "selfRegistration": "로컬 계정 가입 허용", + "registrationRequiresLogin": "비밀번호 로그인이 꺼져 있어 현재 가입할 수 없습니다. 가입 설정은 유지됩니다.", + "lockoutWarning": "비밀번호만 사용하는 관리자는 변경 후 다시 로그인할 수 없습니다. 먼저 외부 로그인과 최고 관리자 접근을 확인하세요. 기존 세션은 만료될 때까지 유지됩니다.", + "initialRoleRules": "외부 계정 초기 역할 규칙", + "provider": "외부 로그인 제공자", + "email": "검증된 이메일", + "role": "플랫폼 역할", + "addRule": "규칙 추가", + "disable": "비활성화", + "disableConfirm": "이 초기 역할 규칙을 비활성화하시겠습니까?", + "saved": "저장됨", + "saveFailed": "저장 실패", + "loadFailed": "설정을 불러오지 못했습니다. 새로고침한 후 다시 시도하세요.", + "capabilitiesUnavailable": "로그인 설정을 불러오지 못했습니다. 새로고침한 후 다시 시도하세요.", + "registrationDisabled": "로컬 가입이 비활성화되었습니다. 다른 사용 가능한 로그인 방식을 이용하세요.", + "passwordDisabled": "로컬 비밀번호 로그인이 비활성화되었습니다. 다른 사용 가능한 로그인 방식을 이용하세요.", + "status": { + "ACTIVE": "대기 중", + "DISABLED": "비활성화됨", + "CONSUMED": "부여됨" + } + }, + "user": { + "menu": { + "dashboard": "대시보드", + "mySkills": "내 스킬", + "mySuites": "내 스킬 모음", + "myNamespaces": "내 네임스페이스", + "governance": "거버넌스 센터", + "stars": "즐겨찾기됨", + "subscriptions": "내 구독", + "reviewProgress": "내 검토 진행 상황", + "reviews": "검토 관리", + "promotions": "승격 관리", + "reports": "신고 관리", + "users": "사용자 관리", + "labels": "라벨 관리", + "systemConfig": "시스템 설정", + "auditLog": "감사 로그", + "security": "보안 설정", + "profile": "프로필 설정", + "notifications": "알림 설정", + "accounts": "계정 병합", + "logout": "로그아웃", + "namespacesAdmin": "네임스페이스 관리" + } + }, + "footer": { + "license": "라이선스", + "contributing": "기여 안내", + "changelog": "변경 내역", + "product": "제품", + "marketplace": "스킬 마켓플레이스", + "publish": "스킬 게시", + "developers": "개발자", + "project": "프로젝트", + "codeOfConduct": "행동 강령", + "resources": "리소스", + "docs": "문서", + "api": "API", + "community": "커뮤니티", + "privacy": "개인정보처리방침", + "terms": "이용약관", + "copyright": "© 2026 SkillHub. Apache-2.0." + }, + "publish": { + "retryPackagePreview": "다시 시도", + "packageName": "패키지 이름", + "displayName": "표시 이름", + "displayNamePlaceholder": "예: 회의록 도우미", + "displayNameHelp": "최대 200자이며 기본값은 패키지 제목입니다. 변경 사항은 이 버전의 검토를 따르며 식별자나 패키지 파일을 변경하지 않습니다. 비워 두면 패키지 제목을 사용합니다.", + "readingPackage": "패키지 메타데이터를 읽는 중…", + "examplesReady": "전체 언어에서 작업 예제 {{count}}개를 인식했습니다.", + "examplesNone": "제공된 작업 예제가 없습니다. 게시와 사용은 계속 가능합니다.", + "examplesInvalid": "예제 형식을 인식하지 못했습니다. 파일은 일반 파일로 유지되며 게시할 수 있습니다.", + "packagePreviewFailed": "패키지를 읽을 수 없습니다. SKILL.md를 확인하고 패키지를 다시 선택하거나 재시도하세요.", + "title": "스킬 게시", + "subtitle": "SkillHub에 스킬 패키지 업로드", + "reviewNotice": { + "title": "검토 안내", + "description": "제출된 스킬 패키지는 게시 전에 관리자 검토가 필요합니다." + }, + "resubmitNotice": { + "title": "{{skill}} v{{version}} 재제출", + "description": "스킬 이름과 버전을 유지한 수정 패키지를 업로드하세요. 일반 검토 절차로 다시 진행됩니다." + }, + "namespace": "네임스페이스", + "selectNamespace": "네임스페이스 선택", + "visibility": "공개 범위", + "visibilityOptions": { + "public": "공개", + "namespaceOnly": "네임스페이스 전용", + "loggedInUsersOnly": "로그인 사용자 전용", + "private": "비공개" + }, + "file": "스킬 패키지 파일", + "removeSelectedFile": "선택한 파일 제거", + "publishing": "게시하는 중...", + "confirm": "게시 확인", + "success": "게시 성공", + "successDescription": "{{skill}}의 검토를 요청했습니다. 관리자 승인 후 사용할 수 있습니다", + "publishedTitle": "게시 성공", + "publishedDescription": "{{skill}}이 게시되어 다운로드할 수 있습니다", + "pendingReviewTitle": "검토를 요청했습니다", + "pendingReviewDescription": "{{skill}}의 검토를 요청했습니다. 관리자 승인 후 사용할 수 있습니다", + "error": "게시 실패", + "timeoutTitle": "게시 시간 초과", + "timeoutDescription": "게시 요청이 너무 오래 걸렸습니다. 나중에 스킬 목록을 확인하거나 다시 시도하세요.", + "versionExistsTitle": "버전이 이미 존재합니다", + "versionExistsDescription": "이 스킬 버전은 이미 게시되었습니다. SKILL.md의 버전을 변경하고 패키지를 다시 만든 다음 업로드하세요.", + "precheckFailedTitle": "게시 전 검사 실패", + "precheckFailedDescription": "패키지에 비밀 정보, 토큰 또는 비밀번호가 포함된 것으로 보입니다. 실제 자격 증명을 예시 값으로 바꾼 후 다시 시도하세요.", + "warningConfirmTitle": "게시 전 경고", + "warningConfirmDescription": "다음 위험 안내를 확인했습니다. 내용을 이해하고 진행하려면 게시를 계속할 수 있습니다.", + "warningConfirmContinue": "게시 계속", + "warningConfirmCancel": "돌아가서 수정", + "frontmatterFailedTitle": "SKILL.md 형식이 올바르지 않습니다", + "frontmatterFailedDescription": "SKILL.md 상단의 YAML frontmatter를 확인하세요. 필드 값에 콜론이 포함되면 따옴표로 감싸세요.", + "selectRequired": "네임스페이스와 파일을 선택하세요", + "folderPackagingFailed": "선택한 폴더를 패키지로 만들지 못했습니다. 파일이 포함되어 있는지 확인하세요.", + "updateTitle": "스킬 업데이트", + "updateSubtitle": "아바타 변경만 저장하거나 패키지를 업로드하여 새 버전을 게시하세요.", + "optionalPackage": "아바타만 변경할 때는 패키지가 필요하지 않습니다.", + "saveAvatar": "아바타 저장", + "avatarSaved": "아바타 변경을 제출했습니다" + }, + "toast": { + "success": "성공", + "error": "오류", + "warning": "경고", + "info": "안내" + }, + "dialog": { + "confirm": "확인", + "processing": "처리하는 중…", + "cancel": "취소", + "delete": "삭제", + "close": "닫기" + }, + "review": { + "avatarUpdate": "아바타 업데이트", + "avatarRemoval": "아바타 제거", + "metadataChange": "아바타 변경", + "avatarUnifiedDecision": "아바타에도 이 신청의 결정이 적용됩니다. 승인하면 변경 사항이 적용되고 반려하면 현재 아바타가 유지됩니다.", + "currentAvatar": "현재 아바타", + "requestedAvatar": "요청한 아바타", + "avatarPreviewUnavailable": "미리보기 사용 불가", + "approveDisabledAvatar": "아바타 심사가 아직 승인 가능한 상태가 아닙니다.", + "avatarOverrideReason": "자동 심사에서 이 이미지를 차단했습니다. 이를 재정의하려면 검토 의견을 입력하세요.", + "avatarReviewStatus": { + "NOT_REQUIRED": "수동 검토 불필요", + "PENDING": "수동 검토 대기", + "APPROVED": "수동 검토 승인", + "REJECTED": "수동 검토 반려", + "APPEAL_PENDING": "수동 이의 제기 대기" + }, + "detail": "검토 상세 정보", + "tabContent": "스킬 콘텐츠", + "tabChanges": "변경 사항", + "tabSourceSecurity": "출처 및 보안", + "tabHistory": "검토 내역", + "scanSummary": "보안 검사", + "scanLoading": "불러오는 중", + "complianceChanges": "규정 준수 및 버전 상세 정보", + "documentationTooLarge": "이 문서 파일은 너무 커서 여기에 표시할 수 없습니다. 다운로드하여 내용을 확인하세요.", + "documentationLoadFailed": "문서 미리보기를 불러오지 못했습니다. 파일을 다운로드하여 확인하세요.", + "documentationTitle": "스킬 문서", + "technicalDetails": "기술 상세 정보", + "reviewResult": "검토 결과", + "suiteChangesUnavailable": "이 스킬 모음 검토에는 별도 변경 기록이 없습니다. 콘텐츠 및 파일에서 스킬 모음 스냅샷을 확인하세요.", + "id": "검토 ID", + "backToList": "목록으로 돌아가기", + "namespace": "네임스페이스 / 슬러그", + "version": "버전", + "status": "상태", + "statusPending": "대기 중", + "statusApproved": "승인됨", + "statusRejected": "반려됨", + "statusSuperseded": "새 버전으로 대체됨", + "submitter": "제출자", + "submitTime": "제출 시각", + "reviewer": "검토자", + "reviewTime": "검토 시각", + "reviewComment": "검토 의견", + "attemptHistory": "제출 내역", + "attemptHistoryError": "제출 내역을 불러오지 못했습니다. 나중에 다시 시도하세요.", + "githubSourceTitle": "GitHub 동기화 소스", + "githubSourceReadOnly": "수집된 원본 파일은 읽기 전용입니다. 출처, 파일 변경 사항, 라이선스 근거를 확인하세요.", + "githubRepository": "저장소", + "githubSkillPath": "스킬 경로", + "githubCommit": "고정된 커밋", + "githubRevision": "콘텐츠 리비전", + "githubRevisionValue": "리비전 {{count}}", + "githubRunId": "수집 실행 ID", + "scanPending": "보안 검사가 진행 중입니다. 완료되면 결과가 표시됩니다.", + "scanFailed": "보안 검사에 실패하여 신뢰할 수 있는 결과가 없습니다.", + "scanUnavailable": "아직 보안 검사 결과가 없습니다.", + "scanLoadFailed": "보안 검사 결과를 불러오지 못했습니다. 나중에 다시 시도하세요.", + "githubLicenseStatus": "라이선스 근거 상태", + "githubLicenseRECOGNIZED": "인식됨", + "githubLicenseMISSING": "라이선스 파일을 찾지 못함", + "githubLicensePRESENT_UNCLASSIFIED": "파일 발견: 라이선스 미분류", + "githubLicensePRESENT_UNPREVIEWABLE": "파일 발견: 텍스트 미리보기 불가", + "githubLicenseWarning": "라이선스가 확인되지 않았습니다. 플랫폼으로 가져오는 경우 검토 의견에 승인 근거를 설명하세요.", + "githubLicenseBasisLabel": "라이선스 판단 근거(필수)", + "githubLicenseBasisRequired": "플랫폼 가져오기를 승인하기 전에 라이선스 판단 근거를 설명하세요.", + "githubFileChanges": "게시된 버전 대비 변경 사항", + "githubChangesDescription": "미리 볼 수 있는 파일을 선택하여 게시된 버전과 텍스트를 비교하세요.", + "githubFirstImportDescription": "최초 가져오기이므로 게시된 기준 버전이 없습니다. 미리 볼 수 있는 파일을 선택하여 읽으세요.", + "githubChangedFilesCount": "파일 {{count}}개", + "githubFirstImport": "초기 파일 트리", + "githubNoFileChanges": "파일 변경 사항이 없습니다.", + "githubSelectPreviewableFile": "변경 사항을 보려면 미리 볼 수 있는 파일을 선택하세요", + "githubSelectFile": "변경된 파일 선택", + "githubPreviousFile": "이전 파일", + "githubNextFile": "다음 파일", + "githubViewMode": "미리보기 방식", + "githubViewChanges": "변경 사항", + "githubViewFull": "전체 파일", + "githubChangeADDED": "추가됨", + "githubChangeMODIFIED": "수정됨", + "githubChangeREMOVED": "제거됨", + "githubLicenseDocuments": "라이선스 및 NOTICE 근거", + "githubNoLicenseDocuments": "표시할 근거 파일이 없습니다.", + "githubLicensePreviewUnavailable": "이 파일의 텍스트 미리보기를 사용할 수 없습니다.", + "githubSourceError": "동기화 소스를 불러오지 못했습니다. 나중에 다시 시도하세요.", + "githubDiffLoading": "텍스트 차이를 불러오는 중…", + "githubDiffError": "텍스트 차이를 불러오지 못했습니다. 나중에 다시 시도하세요.", + "githubLargeDiffNotice": "파일이 크거나 줄 수가 많아 줄 단위 비교를 건너뛰었습니다. 현재 파일 내용이 아래에 표시됩니다.", + "githubBaseFileUnavailable": "삭제된 이 파일은 기준 파일 크기 제한인 1 MiB를 초과하거나 크기가 기록되지 않아 여기에서 미리 보거나 다운로드할 수 없습니다.", + "actions": "검토 작업", + "commentLabel": "의견(선택 사항)", + "commentPlaceholder": "검토 의견 입력...", + "approve": "승인", + "reject": "반려", + "confirmReject": "반려 확인", + "cancelReject": "취소", + "rejectReasonRequired": "반려 시 사유를 입력해야 합니다", + "approveTitle": "검토 승인", + "approveDescription": "이 검토를 승인하시겠습니까?", + "approveConfirm": "승인", + "rejectTitle": "검토 반려", + "rejectDescription": "이 검토를 반려하시겠습니까?", + "rejectConfirm": "반려", + "approveSuccess": "검토를 승인했습니다", + "approveFailed": "승인 실패", + "rejectSuccess": "검토를 반려했습니다", + "rejectFailed": "반려 실패", + "approveDisabledScanning": "보안 검사가 진행 중입니다. 검사가 완료되면 승인할 수 있습니다.", + "notFound": "검토 작업을 찾을 수 없습니다", + "skillDetailTitle": "스킬 상세 정보", + "skillDetailDescription": "아래 섹션은 이 검토 작업에 연결된 대기 버전으로 고정됩니다: {{skill}}", + "skillDetailError": "이 검토의 스킬 상세 정보를 불러오지 못했습니다.", + "activeReviewVersion": "검토 버전", + "downloadSkillZip": "스킬 ZIP 다운로드", + "complianceDiffTitle": "규정 준수 선언 비교", + "complianceDiffDescription": "게시된 {{baseVersion}} 버전과 대기 중인 {{pendingVersion}} 버전의 규정 준수 변경 사항을 비교합니다.", + "complianceDiffDescriptionSync": "게시된 콘텐츠와 동기화 대기 콘텐츠의 규정 준수 선언을 비교합니다.", + "complianceDiffBaseVersion": "기준 버전", + "complianceDiffPendingVersion": "대기 버전", + "complianceDiffBaseDigest": "기준 해시", + "complianceDiffPendingDigest": "대기 버전 해시", + "complianceDiffAdded": "추가됨", + "complianceDiffRemoved": "제거됨", + "complianceDiffModified": "수정됨", + "complianceDiffAddedLabel": "추가 {{count}}개", + "complianceDiffRemovedLabel": "제거 {{count}}개", + "complianceDiffModifiedLabel": "수정 {{count}}개", + "complianceDiffViewDetails": "상세 보기", + "complianceDiffBaseRemoved": "이 선언은 기준 버전에 있으며 대기 버전에서는 제거되었습니다.", + "complianceDiffPendingAdded": "이 선언은 대기 버전에서 추가되었습니다.", + "noDocumentation": "이 검토 버전에는 읽을 수 있는 문서 파일이 없습니다." + }, + "token": { + "title": "API 토큰", + "createNew": "토큰 만들기", + "copyHint": "보안을 위해 토큰 원문은 생성 시에만 복사할 수 있습니다. 나중에 다시 보거나 복사할 수 없습니다. 필요하면 새 토큰을 만드세요.", + "empty": "아직 생성된 토큰이 없습니다", + "emptyHint": "위 버튼을 클릭하여 첫 토큰을 만드세요", + "name": "이름", + "prefix": "토큰 접두사", + "createdAt": "생성 시각", + "lastUsed": "마지막 사용", + "expiresAt": "만료 시각", + "neverExpires": "만료 없음", + "actions": "작업", + "copy": "복사", + "copySuccess": "토큰을 클립보드에 복사했습니다", + "copyFailed": "토큰을 복사하지 못했습니다. 다시 시도하세요.", + "copyUnavailableTitle": "이전 토큰 원문은 다시 복사할 수 없습니다", + "copyUnavailableDescription": "토큰 원문은 생성 직후 한 번만 표시됩니다. 더 이상 가지고 있지 않다면 새 토큰을 만드세요.", + "editExpiration": "만료 편집", + "editExpirationTitle": "토큰 만료 편집", + "editExpirationDescription": "토큰 \"{{name}}\"의 새 만료 시각을 설정하세요.", + "saveExpiration": "만료 저장", + "updatingExpiration": "저장하는 중...", + "updateExpirationSuccess": "토큰 만료를 변경했습니다", + "updateExpirationFailed": "토큰 만료를 변경하지 못했습니다", + "delete": "삭제", + "deleteTitle": "토큰 삭제", + "deleteDescription": "토큰 \"{{name}}\"을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "deleteSuccess": "토큰을 삭제했습니다", + "deleteFailed": "삭제 실패", + "loading": "불러오는 중..." + }, + "apiError": { + "unauthorized": "세션이 만료되었습니다. 다시 로그인하세요", + "forbidden": "이 작업을 수행할 권한이 없습니다", + "notFound": "요청한 리소스를 찾을 수 없습니다", + "serverError": "서버 오류가 발생했습니다. 나중에 다시 시도하세요", + "networkError": "네트워크 연결에 실패했습니다. 네트워크를 확인하세요", + "unknown": "작업 실패" + }, + "routeGuard": { + "forbiddenTitle": "이 페이지를 볼 권한이 없습니다", + "forbiddenDescription": "계정 권한이 변경되었습니다. 이전 페이지로 돌아갑니다." + }, + "securityAudit": { + "title": "보안 감사", + "dialogDescription": "이 버전의 상세 보안 검사 결과입니다.", + "findings": "발견 항목", + "findingsCount": "발견 항목 {{count}}개", + "totalFindings": "발견 항목 총 {{count}}개", + "scanDuration": "{{seconds}}초", + "statusScanning": "검사 중", + "statusScanFailed": "검사 실패", + "failureReason": "사유: {{reason}}", + "retry": "검사 재시도", + "retrying": "재시도하는 중...", + "retrySuccess": "보안 검사를 다시 시작했습니다", + "retryError": "보안 검사를 다시 시도하지 못했습니다", + "remediation": "개선 방안", + "viewDetails": "상세 보기", + "verdict": { + "SAFE": "고위험 항목이 발견되지 않음", + "SUSPICIOUS": "의심스러움", + "DANGEROUS": "위험", + "BLOCKED": "고위험" + }, + "severity": { + "CRITICAL": "치명적", + "HIGH": "높음", + "MEDIUM": "보통", + "LOW": "낮음", + "INFO": "안내" + } + }, + "error": { + "auth": { + "local": { + "invalidCredentials": "사용자 이름 또는 비밀번호가 올바르지 않습니다", + "accountDisabled": "비활성화된 계정입니다", + "accountPending": "활성화 대기 중인 계정입니다", + "accountMerged": "병합된 계정이므로 더 이상 로그인할 수 없습니다", + "locked": "실패 횟수가 너무 많습니다. 나중에 다시 시도하세요" + }, + "direct": { + "disabled": "직접 인증 호환 기능이 비활성화되었습니다", + "providerUnsupported": "지원하지 않는 로그인 방식입니다" + }, + "sessionBootstrap": { + "disabled": "세션 부트스트랩이 비활성화되었습니다", + "providerUnsupported": "지원하지 않는 SSO 부트스트랩 방식입니다", + "notAuthenticated": "인증된 외부 세션을 찾지 못했습니다" + } + } + }, + "routeError": { + "title": "오류가 발생했습니다", + "description": "페이지에서 예상치 못한 오류가 발생했습니다. 초기화하여 계속하세요. 확장 프로그램이 없는 시크릿 창에서도 문제가 계속되면 신고해 주세요.", + "reset": "초기화" + }, + "notification": { + "title": "알림", + "empty": "알림 없음", + "markAllRead": "모두 읽음으로 표시", + "deleteRead": "읽은 알림 삭제", + "viewAll": "모든 알림 보기", + "unread": "읽지 않음", + "all": "전체", + "publish": "게시", + "review": "검토", + "promotion": "승격", + "report": "신고", + "timeAgo": "{{time}}", + "preferences": { + "title": "알림 설정", + "description": "알림 환경설정을 관리하세요", + "publish": "게시 알림", + "publishDesc": "스킬이 게시되면 알림", + "review": "검토 알림", + "reviewDesc": "검토 제출, 승인, 반려 시 알림", + "promotion": "승격 알림", + "promotionDesc": "승격 요청, 승인, 반려 시 알림", + "report": "신고 알림", + "reportDesc": "신고 제출 또는 처리 시 알림" + } + }, + "adminNamespaces": { + "title": "네임스페이스 관리", + "subtitle": "관리 화면에서 모든 네임스페이스를 확인하고 멤버, 수명 주기, 거버넌스 작업을 관리하세요.", + "statTotal": "전체 네임스페이스", + "statActive": "활성", + "statFrozen": "동결됨", + "statArchived": "보관됨", + "searchLabel": "네임스페이스 검색", + "searchPlaceholder": "슬러그, 표시 이름 또는 설명으로 검색", + "searchAction": "검색", + "clearSearch": "지우기", + "statusFilter": "상태", + "typeFilter": "유형", + "filterAll": "전체", + "typeGlobal": "글로벌", + "typeTeam": "팀", + "colNamespace": "네임스페이스", + "colType": "유형", + "colStatus": "상태", + "colMembers": "멤버", + "colSkills": "스킬", + "colUpdated": "업데이트됨", + "empty": "현재 필터에 맞는 네임스페이스가 없습니다", + "selectNamespace": "상세 정보를 보려면 네임스페이스를 선택하세요", + "members": "멤버", + "skills": "스킬", + "currentRole": "현재 역할", + "platformOverride": "플랫폼 권한 우선 적용", + "yes": "예", + "no": "아니요", + "noMembership": "멤버 아님", + "createdAt": "생성됨", + "updatedAt": "업데이트됨", + "membersTitle": "멤버 관리", + "membersDescription": "플랫폼 관리자는 팀 네임스페이스의 멤버와 역할을 관리할 수 있습니다. 글로벌 네임스페이스는 읽기 전용으로 유지됩니다.", + "governanceTitle": "거버넌스 작업", + "governanceDescription": "동결, 보관, 복원은 게시와 일반 공개 상태에 영향을 줍니다. 작업은 감사 기록에 남습니다.", + "reasonLabel": "사유", + "reasonPlaceholder": "감사 검토를 위해 이 거버넌스 작업이 필요한 이유를 설명하세요.", + "memberRoleUpdated": "멤버 역할을 변경했습니다", + "freezeAction": "네임스페이스 동결", + "unfreezeAction": "네임스페이스 동결 해제", + "archiveAction": "네임스페이스 보관", + "restoreAction": "네임스페이스 복원", + "freezeConfirmTitle": "네임스페이스를 동결하시겠습니까?", + "freezeConfirmDescription": "“{{name}}”이 읽기 전용으로 전환되며 멤버는 새 스킬 버전을 게시할 수 없습니다.", + "unfreezeConfirmTitle": "네임스페이스의 동결을 해제하시겠습니까?", + "unfreezeConfirmDescription": "“{{name}}”에서 다시 게시하고 멤버를 관리할 수 있습니다.", + "archiveConfirmTitle": "네임스페이스를 보관하시겠습니까?", + "archiveConfirmDescription": "“{{name}}”이 일반 서비스 화면에서 숨겨지며 관리 화면에서 계속 표시됩니다.", + "restoreConfirmTitle": "네임스페이스를 복원하시겠습니까?", + "restoreConfirmDescription": "“{{name}}”이 일반 서비스 화면에 다시 표시됩니다.", + "freezeSuccessTitle": "네임스페이스를 동결했습니다", + "freezeSuccessDescription": "“{{name}}”이 읽기 전용으로 전환되었습니다.", + "unfreezeSuccessTitle": "네임스페이스의 동결을 해제했습니다", + "unfreezeSuccessDescription": "“{{name}}”이 다시 활성화되었습니다.", + "archiveSuccessTitle": "네임스페이스를 보관했습니다", + "archiveSuccessDescription": "“{{name}}”이 일반 서비스 화면에서 숨겨졌습니다.", + "restoreSuccessTitle": "네임스페이스를 복원했습니다", + "restoreSuccessDescription": "“{{name}}”이 다시 표시됩니다.", + "freezeErrorTitle": "네임스페이스를 동결하지 못했습니다", + "unfreezeErrorTitle": "네임스페이스의 동결을 해제하지 못했습니다", + "archiveErrorTitle": "네임스페이스를 보관하지 못했습니다", + "restoreErrorTitle": "네임스페이스를 복원하지 못했습니다" + }, + "suite": { + "workspace": { + "description": "스킬 모음, 버전, 생성 진행 상황을 관리하세요.", + "searchLabel": "내 스킬 모음 검색", + "searchPlaceholder": "이름, 네임스페이스 또는 슬러그 검색", + "clearSearch": "검색어 지우기", + "stateFilter": "스킬 모음 상태", + "attention": "확인 필요", + "filter": { + "ALL": "모든 상태", + "ATTENTION": "확인 필요", + "DRAFT": "편집 중", + "PENDING_REVIEW": "검토 중", + "PUBLISHED": "게시됨", + "OTHER": "기타 상태" + }, + "columns": { + "suite": "스킬 모음", + "version": "대상 버전", + "state": "현재 상태", + "updated": "업데이트됨", + "action": "작업" + }, + "reviewProgress": "검토 진행 상황", + "resourceSuite": "스킬 모음", + "resourceSkill": "스킬", + "total": "총 {{count}}개, 페이지당 {{size}}개", + "noMatches": "일치하는 스킬 모음 없음", + "error": "스킬 모음을 불러올 수 없습니다", + "reload": "다시 불러오기" + }, + "statusLabel": { + "DRAFT": "편집 중", + "PENDING_REVIEW": "검토 중", + "PUBLISHED": "게시됨", + "REJECTED": "승인되지 않음", + "YANKED": "내려짐" + }, + "listTitle": "스킬 모음", + "listDescription": "버전이 고정된 스킬 모음을 한 번에 설치하세요.", + "create": "스킬 모음 만들기", + "searchPlaceholder": "스킬 모음 이름 또는 설명 검색", + "emptyTitle": "사용 가능한 스킬 모음 없음", + "emptyDescription": "첫 스킬 모음을 만들어 재사용 가능한 스킬 워크플로를 구성하세요.", + "noSummary": "설명 없음", + "assignedLabels": "스킬 모음 라벨", + "labelFilterTitle": "스킬 모음 라벨별 필터", + "allLabels": "모든 라벨", + "loadingLabels": "라벨을 불러오는 중…", + "labelsSectionTitle": "스킬 모음 라벨 관리", + "labelsSectionDescription": "라벨은 이 스킬 모음에만 적용되며 구성 스킬을 변경하지 않습니다.", + "labelsSectionDescriptionSuperAdmin": "일반 또는 권한 제한 라벨을 관리하세요. 이 스킬 모음에만 적용됩니다.", + "currentLabelsTitle": "현재 라벨", + "availableLabelsTitle": "사용 가능한 라벨", + "loadingAvailableLabels": "사용 가능한 라벨을 불러오는 중…", + "noLabelsAssigned": "이 스킬 모음에 지정된 라벨이 없습니다.", + "noAvailableLabels": "현재 추가할 수 있는 라벨이 더 없습니다.", + "addLabel": "{{label}} 추가", + "removeLabel": "제거", + "labelRestrictedHint": "최고 관리자만 제거할 수 있습니다", + "labelAttachSuccessTitle": "스킬 모음 라벨을 추가했습니다", + "labelAttachSuccessDescription": "이제 직접 연결된 라벨을 스킬 모음 탐색에 사용합니다.", + "labelAttachErrorTitle": "스킬 모음 라벨을 추가하지 못했습니다", + "labelDetachSuccessTitle": "스킬 모음 라벨을 제거했습니다", + "labelDetachSuccessDescription": "구성 스킬과 해당 라벨은 변경되지 않았습니다.", + "labelDetachErrorTitle": "스킬 모음 라벨을 제거하지 못했습니다", + "labelActionFallbackError": "다시 시도하세요.", + "processing": "처리하는 중…", + "requiredForPublish": "게시에 필요", + "publishReadinessTitle": "게시 준비 상태", + "publishReadinessDescription": "초안은 미완성이어도 되지만 검토나 직접 게시 전에는 요약과 개요를 작성해야 합니다.", + "summaryComplete": "요약 작성 완료", + "summaryIncomplete": "요약 작성 필요", + "overviewComplete": "개요 작성 완료", + "overviewIncomplete": "개요 작성 필요", + "unavailable": "사용 불가", + "notFound": "스킬 모음이 존재하지 않거나 계정에 접근 권한이 없습니다.", + "published": "스킬 모음을 게시했습니다", + "submitted": "스킬 모음의 검토를 요청했습니다", + "actionFailed": "작업 실패", + "editDraft": "초안 편집", + "publishDirectly": "게시", + "submitReview": "검토 요청", + "version": "버전", + "status": "상태", + "visibility": "공개 범위", + "installStatus": "설치 상태", + "available": "사용 가능", + "degraded": "기능 저하", + "installCommand": "설치 명령어", + "copyInstallCommand": "설치 명령어 복사", + "detailsSidebar": "스킬 모음 정보 및 작업", + "commandCopied": "설치 명령어를 복사했습니다", + "degradedDescription": "구성 스킬 또는 접근 권한이 변경되었습니다. 이 스킬 모음을 설치하려면 모든 구성 스킬을 복원하세요.", + "membersTitle": "포함된 스킬({{count}}개)", + "overviewTab": "개요", + "membersTab": "스킬 모듈({{count}}개)", + "overviewTitle": "스킬 모음 개요", + "noOverview": "작성자가 상세 개요를 제공하지 않았습니다.", + "viewMember": "스킬 보기: {{name}}", + "pinnedVersion": "고정 버전 v{{version}}", + "entrySkill": "진입점 스킬", + "startWithEntry": "진입점 스킬부터 시작하세요", + "startWithEntryDescription": "이 스킬 모음을 설치하면 버전이 고정된 스킬 {{count}}개를 사용할 수 있습니다. 먼저 진입점 스킬을 열어 함께 사용하는 기본 방법을 확인하세요.", + "viewEntrySkill": "진입점 스킬 보기", + "versionHistory": "버전 내역", + "installSuite": "스킬 모음 설치", + "currentVersion": "현재 버전", + "membersTabShort": "구성 스킬 {{count}}개", + "versionsTab": "버전 {{count}}개", + "overviewSupport": "현재 스킬 모음 버전 상세 정보", + "entrySnapshotDescription": "진입점 스킬은 이 스킬 모음에서 참조하는 정확한 버전으로 고정됩니다.", + "thisVersionContains": "이 버전의 구성", + "memberCount": "스킬", + "membersSnapshotDescription": "이 스킬 모음은 다음 스킬의 정확한 버전을 고정하며 자동으로 업그레이드하지 않습니다.", + "pinnedVersionColumn": "고정 버전", + "memberPurpose": "스킬 모음 내 용도", + "availability": "사용 가능 여부", + "memberAction": "작업", + "entryMemberPurpose": "스킬 모음 설치 후 가장 먼저 사용할 스킬입니다.", + "memberPurposeFallback": "스킬 모음 워크플로에서 독립적인 기능을 제공합니다.", + "availableForSuite": "스킬 모음에서 사용 가능", + "viewPinnedVersion": "고정 버전 보기", + "memberUnavailable": "조회 불가", + "availableMemberCount": "구성 스킬 버전 {{total}}개 중 {{available}}개 사용 가능", + "memberSnapshotHint": "이 참조는 현재 스킬 모음 버전에 고정되어 있습니다.", + "lifecycleStatus": "수명 주기 상태", + "createdBy": "생성자", + "createdAt": "생성 시각", + "updatedAt": "업데이트 시각", + "noChangelog": "릴리스 노트 없음", + "viewVersion": "버전 보기", + "versionDetails": "버전 상세 정보", + "memberSnapshot": "구성 스냅샷(스킬 {{count}}개)", + "memberInformation": "구성 스킬({{count}}개)", + "memberChange": "구성 변경", + "loadingVersionDetails": "버전 상세 정보를 불러오는 중", + "memberChangeKinds": { + "ADDED": "추가됨", + "UPDATED": "v{{version}}에서 업그레이드됨", + "UNCHANGED": "변경 없음", + "REMOVED": "제거됨" + }, + "validationRequired": "필수 항목을 입력하세요", + "namespaceRequired": "네임스페이스를 선택하세요", + "slugRequired": "스킬 모음 슬러그를 입력하세요", + "nameRequired": "스킬 모음 이름을 입력하세요", + "versionRequired": "새 버전 번호를 입력하세요", + "membersRequired": "스킬을 하나 이상 선택하세요", + "draftUpdated": "스킬 모음 초안을 업데이트했습니다", + "draftCreated": "스킬 모음 초안을 만들었습니다", + "saveFailed": "저장 실패", + "editTitle": "스킬 모음 편집", + "createTitle": "스킬 모음 만들기", + "newVersionTitle": "스킬 모음 버전 만들기", + "newVersionPlaceholder": "새 버전 입력(예: 1.1.0)", + "sourceLoadFailed": "원본 스킬 모음 버전을 불러올 수 없습니다. 상세 페이지로 돌아가 다시 시도하세요.", + "editorAccessDenied": "이 계정으로는 해당 스킬 모음 버전을 편집할 수 없습니다.", + "editorDescription": "게시된 스킬의 정확한 버전을 선택하세요. 게시된 스킬 모음은 자동으로 새 버전으로 변경되지 않습니다.", + "authoringMode": "스킬 모음 작성 방식", + "manualAuthoring": "직접 구성", + "localImport": "로컬에서 가져오기", + "tabs": { + "suites": "스킬 모음", + "publishing": "게시 작업" + }, + "bundle": { + "uploadTitle": "스킬 모음 번들 업로드", + "uploadDescription": "ZIP 파일 하나 또는 SUITE.yaml과 여러 스킬 폴더를 포함한 루트 폴더를 선택하세요. 브라우저에서 ZIP 파일 하나만 생성하여 업로드합니다.", + "processing": "번들을 패키징하고 검증하는 중", + "previewTitle": "구성 변경 미리보기", + "previewFailed": "번들 미리보기 실패", + "packageFailed": "선택한 폴더를 패키징하지 못했습니다", + "confirmFailed": "번들 확인 실패", + "confirmable": "확인 준비 완료", + "notConfirmable": "확인 불가", + "targetMismatch": "번들 방식 또는 대상 스킬 모음이 이 페이지와 일치하지 않습니다. 올바른 번들을 선택하세요.", + "previewExpired": "미리보기가 만료되었습니다. 번들을 다시 선택하여 업로드하세요.", + "noChanges": "모든 구성 스킬과 고정 버전에 변경 사항이 없습니다.", + "removedMember": "{{coordinate}}@{{version}} 제거", + "removedEntryMember": "현재 진입점 스킬", + "memberDirectory": "구성 디렉터리: {{path}}", + "acceptMemberWarnings": "{{coordinate}}의 경고 {{count}}개를 검토하고 수락합니다", + "acceptRemovals": "구성 스킬 {{removalCount}}개 제거를 검토하고 수락합니다", + "confirm": "확인 및 게시 시작", + "chooseAgain": "다시 선택", + "progressTitle": "번들 게시 진행 상황", + "activeTitle": "게시 중인 스킬 모음", + "activeDescription": "이 번들은 스킬 모음 초안을 만들기 전에 구성 스킬의 준비를 기다리고 있습니다.", + "memberProgress": "구성 스킬 {{total}}개 중 {{completed}}개 완료, {{waiting}}개 검사 또는 검토 대기", + "continueOperation": "게시 진행 상황 보기", + "loadingOperation": "작업 상태를 불러오는 중", + "operationLoadFailed": "번들 작업을 불러올 수 없습니다. 연결을 확인한 후 다시 시도하세요.", + "operationTargetMismatch": "복원된 번들 작업이 이 스킬 모음과 일치하지 않습니다. 복구 항목을 지웠습니다.", + "reloadOperation": "다시 불러오기", + "redactedMember": "접근 정책으로 숨겨진 구성 스킬", + "viewMemberSkill": "구성 스킬 보기", + "memberSkillNotCreated": "아직 생성되지 않은 스킬", + "forgetOperation": "이 작업 복원 중지", + "importAnother": "다른 번들 가져오기", + "openDraft": "스킬 모음 초안 열기", + "retryMemberPublish": "계속", + "retryScan": "다시 확인", + "retrying": "재시도하는 중…", + "retryFailed": "번들 작업을 다시 시도하지 못했습니다", + "cancelOperation": "스킬 모음 생성 중지", + "cancelFailed": "번들 작업을 취소하지 못했습니다", + "openingTask": "게시 작업을 여는 중…", + "taskListTitle": "스킬 모음 게시 작업", + "taskListDescription": "입력이 필요한 작업부터 처리하세요. 나머지는 SkillHub가 자동으로 진행합니다.", + "taskDetailTitle": "스킬 모음 게시 작업", + "taskDetailDescription": "작성 페이지 밖에서도 구성 스킬 게시와 스킬 모음 초안 생성을 확인할 수 있습니다.", + "backToTasks": "게시 작업으로 돌아가기", + "backToSuite": "스킬 모음 상세로 돌아가기", + "taskEmpty": "게시 작업 없음", + "taskEmptyDescription": "스킬 모음 번들을 가져와 확인하면 여기에 게시 진행 상황이 표시됩니다.", + "groups": { + "attention": "먼저 처리할 작업", + "attentionDescription": "차단되거나 변경된 계획을 계속하려면 입력이 필요합니다", + "attentionEmpty": "현재 확인이 필요한 작업이 없습니다.", + "progress": "자동 처리 중", + "progressDescription": "구성 스킬 처리와 검토가 진행 중일 때는 별도 조치가 필요하지 않습니다", + "progressEmpty": "현재 진행 중인 작업이 없습니다.", + "recent": "완료됨", + "recentDescription": "초안이 생성되었거나 작업이 중지되었습니다. 필요할 때 기록을 여세요", + "recentEmpty": "최근 완료된 작업이 없습니다." + }, + "taskCount": "게시 작업 {{count}}개", + "priorityHint": "조치가 필요한 작업이 먼저 표시됩니다", + "taskAction": { + "RUNNING": "진행 상황 보기", + "WAITING_FOR_MEMBERS": "진행 상황 보기", + "BLOCKED_RETRYABLE": "계속", + "REPREVIEW_REQUIRED": "다시 확인", + "SUITE_DRAFT_CREATED": "초안 열기", + "CANCELLED": "기록 보기" + }, + "taskHint": { + "RUNNING": "구성 스킬 게시 중", + "WAITING_FOR_MEMBERS": "구성 스킬 검토 대기 중", + "BLOCKED_RETRYABLE": "미완료 구성 스킬을 열어 재시도", + "REPREVIEW_REQUIRED": "최신 변경 사항을 미리 보고 확인", + "SUITE_DRAFT_CREATED": "편집을 계속하거나 검토 요청", + "CANCELLED": "중지됨: 기록은 계속 조회 가능" + }, + "statusLabel": { + "RUNNING": "처리 중", + "WAITING_FOR_MEMBERS": "구성 스킬 대기 중", + "BLOCKED_RETRYABLE": "구성 스킬 미완료", + "REPREVIEW_REQUIRED": "설정 변경됨", + "SUITE_DRAFT_CREATED": "초안 생성됨", + "CANCELLED": "중지됨" + }, + "problem": { + "memberScanFailed": { + "title": "구성 스킬의 보안 검사에 실패했습니다", + "description": "기존 구성 스킬 버전은 보존됩니다. 검사를 사용할 수 있게 되면 번들을 다시 업로드하지 않고 해당 스킬을 재시도하세요." + }, + "memberExecutionFailed": { + "title": "구성 스킬 게시에 실패했습니다", + "description": "완료된 구성 스킬은 다시 실행하지 않습니다. 아래 미완료 구성 스킬의 문제를 해결한 후 계속하세요." + }, + "authorizationBlocked": { + "title": "권한 또는 네임스페이스 접근이 변경되었습니다", + "description": "현재 계정에 구성 스킬 게시 또는 스킬 모음 관리 권한이 없어졌거나 관련 네임스페이스가 동결되었을 수 있습니다. 접근 권한을 복원하거나 동결을 해제한 후 재시도하세요." + }, + "planChanged": { + "title": "구성 스킬 또는 접근 권한이 변경되었습니다", + "description": "구성 스킬 버전, 권한 또는 스킬 모음 상태가 확인된 계획과 더 이상 일치하지 않습니다. 번들을 다시 가져와 최신 변경 사항을 확인하세요." + }, + "draftMissing": { + "title": "스킬 모음 초안이 생성되지 않았습니다", + "description": "구성 스킬에서 모음이 참조할 수 있는 버전이 생성되지 않았습니다. 구성 패키지를 수정한 후 번들을 다시 가져오세요." + }, + "blocked": { + "title": "아직 구성 스킬 게시를 계속할 수 없습니다", + "description": "아래 구성 목록에 미완료 항목이 표시됩니다. 재시도하면 해당 스킬만 처리합니다. 계속 실패하면 관리자에게 작업 ID를 전달하세요." + }, + "affectedMembers": "처리할 구성 스킬: {{members}}", + "affectedMembersLabel": "확인 필요", + "affectedMembersMore": "{{count}}개 더 보기" + }, + "lifecycleTitle": "스킬 모음 게시 단계", + "lifecycle": { + "preview": "변경 사항 확인", + "previewDescription": "업로드와 구성 변경 사항이 확정되었습니다", + "members": "구성 스킬 게시", + "membersDescription": "스킬 버전을 생성하거나 재사용하고 검토 대기", + "draft": "스킬 모음 초안 생성", + "draftDescription": "모든 구성 스킬이 준비된 후에만 실행" + }, + "operationId": "작업 ID", + "lastUpdated": "마지막 업데이트", + "memberTitle": "구성 스킬", + "memberDescription": "구성 스킬은 독립된 스킬로 게시됩니다. 미완료로 표시된 스킬을 확인하세요.", + "memberAction": "구성 스킬 작업", + "memberStatus": { + "PLANNED": "시작 대기 중", + "RUNNING": "버전 생성 중", + "WAITING_FOR_MEMBER": "게시 중", + "COMPLETED": "스킬 모음에서 사용 가능", + "BLOCKED_RETRYABLE": "조치 필요", + "REPREVIEW_REQUIRED": "확인 필요", + "CANCELLED": "후속 작업 중지됨", + "ATTENTION": "미완료" + }, + "noMembers": "아직 구성 스킬 진행 상황이 없습니다.", + "cancelConfirmTitle": "이 스킬 모음의 생성을 중지하시겠습니까?", + "cancelConfirmDescription": "SkillHub가 구성 스킬 대기를 중지하고 스킬 모음 초안을 생성하지 않습니다. 이미 생성된 스킬 버전과 검토 작업은 유지되며 철회되지 않습니다.", + "cancelConfirmAction": "스킬 모음 생성 중지", + "cancelledTitle": "스킬 모음 생성을 중지했습니다", + "cancelledDescription": "이 작업은 내역에 남습니다. 기존 스킬 버전과 검토 작업은 철회되거나 삭제되지 않습니다.", + "startAgain": "번들 다시 가져오기", + "relationship": { + "ADDED": "구성 스킬 추가", + "UPDATED": "고정 버전 변경", + "UNCHANGED": "변경 없음", + "REMOVED": "멤버 제거" + }, + "action": { + "CREATE_SKILL": "스킬 만들기", + "CREATE_VERSION": "버전 만들기", + "REUSE_VERSION": "기존 버전 재사용", + "REFERENCE_VERSION": "정확한 버전 참조", + "NONE": "게시 쓰기 작업 없음" + }, + "source": { + "PACKAGE": "포함된 구성 패키지", + "REFERENCE": "기존 버전 참조" + }, + "path": { + "noWrite": "스킬 게시 쓰기 작업 없이 기존 버전을 재사용하거나 참조합니다.", + "private": "비공개(PRIVATE) 스킬 버전을 만들고 자동으로 게시하지 않은 채 비공개로 유지합니다.", + "publish": "스킬 버전을 만들고 기존 검사, 게시 또는 검토 절차로 진행합니다. 최종 상태는 진행 상황에서 확인하세요." + }, + "errors": { + "empty-folder": "선택한 폴더 또는 ZIP 파일이 비어 있습니다.", + "mixed-folder-roots": "번들 루트 폴더 하나만 선택하세요.", + "missing-suite-manifest": "루트 폴더에 SUITE.yaml이 없습니다.", + "duplicate-suite-manifest": "번들에는 SUITE.yaml이 하나만 포함될 수 있습니다.", + "too-many-files": "번들에 파일이 너무 많습니다.", + "file-too-large": "번들 파일 중 하나가 10 MB를 초과합니다.", + "bundle-too-large": "번들의 전체 크기가 100 MB를 초과합니다.", + "invalid-zip": ".zip 파일을 선택하세요." + } + }, + "namespace": "네임스페이스", + "slug": "슬러그", + "selectNamespace": "네임스페이스를 선택하세요", + "name": "이름", + "namePlaceholder": "마케팅 행사 워크플로", + "overview": "개요(Markdown)", + "overviewHint": "사용 사례, 구성 스킬 순서, 입력, 출력, 주요 제약 사항을 설명하세요.", + "overviewPromptScenario": "사용 사례: 이 모음은 어떤 작업을 위해 설계되었나요?", + "overviewPromptPreparation": "준비 사항: 어떤 정보, 파일, 환경이 필요한가요?", + "overviewPromptSequence": "구성 역할: 각 스킬은 무엇을 하며 어떤 순서로 사용하나요?", + "overviewPromptInputsOutputs": "입력 및 출력: 사용자가 무엇을 제공하고 무엇을 받나요?", + "overviewPromptBoundaries": "범위와 제한: 어떤 권한, 위험, 부적합한 사용 사례를 고려해야 하나요?", + "entryGuideTitle": "고정된 진입점 스킬 안내", + "entryGuideSource": "콘텐츠는 {{coordinate}}@{{version}}의 SKILL.md에서 가져오며 새 버전으로 전환되지 않습니다.", + "entryGuideLoading": "고정된 진입점 스킬 안내를 불러오는 중", + "entryGuideUnavailable": "이 계정으로는 고정된 진입점 스킬 버전을 읽을 수 없습니다.", + "visibilityPublic": "공개", + "visibilityNamespace": "네임스페이스 전용", + "visibilityPrivate": "비공개", + "summary": "설명", + "summaryLabel": "요약", + "changelog": "릴리스 노트", + "selectSkills": "스킬 선택", + "searchSkills": "사용 가능한 스킬 검색", + "memberSearchDescription": "먼저 검색 범위를 선택한 다음 키워드 또는 필터로 구성 스킬을 추가하세요.", + "memberSearchScope": "구성 스킬 검색 범위", + "scopeMarket": "스킬 마켓플레이스", + "scopeNamespace": "현재 네임스페이스", + "scopeCoordinate": "정확한 식별 경로", + "searchOnSubmitHint": "확인 후에만 검색합니다. 기본적으로 모든 스킬을 불러오지 않습니다", + "memberSearchEmptyTitle": "검색 범위를 선택하세요", + "memberSearchEmptyDescription": "모든 스킬을 불러오지 않도록 키워드, 공개 범위 또는 정확한 식별 경로를 지정한 후 검색하세요.", + "search": "검색", + "memberSkillColumn": "스킬", + "memberDescriptionColumn": "설명", + "memberVersionColumn": "버전", + "memberActionColumn": "작업", + "recommendedCandidate": "추천", + "addMember": "추가", + "noCandidates": "선택한 공개 범위를 충족하는 게시 버전이 없습니다.", + "selectedMembers": "스킬 모음 구성({{count}}/100)", + "selectedMembersDescription": "사용 순서대로 구성 스킬을 정렬하고 진입점 스킬 하나를 선택하세요.", + "expandMembers": "구성 스킬 설정 펼치기", + "collapseMembers": "구성 스킬 설정 접기", + "setEntry": "진입점 스킬로 지정", + "entryRequired": "진입점 스킬 선택", + "confirmVersionUpdateTitle": "고정 버전 변경 확인", + "confirmVersionUpdateDescription": "{{coordinate}}가 v{{from}}에서 v{{to}}로 변경됩니다. 스킬 모음은 새 버전에 고정되며 자동으로 업데이트되지 않습니다.", + "confirmVersionUpdate": "버전 변경", + "moveMemberUp": "{{name}} 위로 이동", + "moveMemberDown": "{{name}} 아래로 이동", + "removeMember": "{{name}} 제거", + "setEntryFor": "{{name}}을 진입점 스킬로 지정", + "addMemberHint": "왼쪽에서 스킬을 하나 이상 추가하세요.", + "cancel": "취소", + "saveDraft": "초안 저장", + "resourceType": "유형", + "allTypes": "전체", + "reviewSnapshot": "스킬 모음 구성 스냅샷", + "reviewSnapshotDescription": "이 정확한 스킬 버전들을 검토하세요. 게시 후 자동으로 업그레이드되지 않습니다.", + "publishable": "게시 가능", + "reviewSnapshotFailed": "스킬 모음 검토 스냅샷을 불러올 수 없습니다.", + "reviewSnapshotRetry": "다시 불러오기", + "myTitle": "내 스킬 모음", + "myDescription": "초안, 검토 대기, 게시된 스킬 모음 버전을 관리하세요.", + "view": "보기", + "myEmpty": "아직 스킬 모음이 없습니다", + "myEmptyDescription": "스킬 모음을 만들어 편집을 계속하고 여기에서 검토 상태를 확인하세요.", + "updatePinnedVersion": "고정 버전 업데이트", + "resourceTypeSuite": "스킬 모음", + "resourceTypeSkill": "스킬", + "managementTitle": "스킬 모음 관리", + "managementDescription": "버전, 게시 상태, 스킬 모음 컨테이너를 관리하세요. 구성 스킬은 변경되지 않습니다.", + "management": { + "backToSuites": "내 스킬 모음으로 돌아가기", + "yankedTitle": "v{{version}}의 배포가 철회되었습니다", + "yankedDescription": "새 사용자는 이 버전을 설치할 수 없습니다. 구성 스킬에는 영향을 주지 않습니다.", + "degradedTitle": "v{{version}}을 설치할 수 없습니다", + "availableTitle": "v{{version}}을 설치할 수 있습니다", + "availableDescription": "이 버전과 모든 구성 스킬을 사용할 수 있습니다.", + "tabs": { + "overview": "스킬 모음 정보", + "members": "구성 스킬 {{count}}개", + "versions": "버전 {{count}}개", + "publishing": "게시 내역 {{count}}개" + }, + "previewMarket": "마켓플레이스 페이지 미리보기", + "draftDescription": "구성 스킬 버전이 고정되었습니다. 게시 정보를 작성하여 검토를 요청하세요.", + "reviewDescription": "이 버전은 검토 중입니다. 추가로 제출할 필요가 없습니다.", + "memberCountValue": "구성 스킬 {{count}}개", + "membersSnapshotDescription": "이 버전은 다음 스킬을 고정합니다. 스킬 모음을 편집해도 구성 스킬은 수정되지 않습니다.", + "editMembers": "구성 스킬 편집", + "memberLifecycleHint": "구성 스킬의 수명 주기는 독립적입니다. 접근 권한이나 사용 가능 여부가 변경되면 게시가 차단될 수 있습니다.", + "memberVersions": "구성 스킬 버전", + "publishChecklist": "게시 체크리스트", + "publishChecklistDescription": "이 버전을 제출하기 전에 다음 항목을 완료하세요.", + "ready": "완료", + "incomplete": "미완료", + "marketContent": "마켓플레이스 콘텐츠", + "marketContentDescription": "미리보기입니다. 전체 내용은 마켓플레이스 페이지에 표시됩니다.", + "editContent": "콘텐츠 편집", + "recentProgress": "최근 진행 상황", + "basicInfo": "기본 정보", + "memberCount": "멤버", + "membersTitle": "구성 스킬({{count}}개)", + "member": "스킬", + "role": "역할", + "memberRole": "구성 스킬", + "normal": "사용 가능", + "recentPublishing": "최근 게시 내역", + "viewAll": "전체 보기", + "openPublishingHint": "게시 내역을 열어 작업 상태 확인", + "publishingDescription": "이 스킬 모음의 번들 게시 작업과 구성 스킬 진행 상황을 확인하세요." + }, + "reopenDraft": "초안으로 다시 열어 편집", + "reopened": "스킬 모음 버전을 초안으로 다시 열었습니다", + "createVersion": "새 버전 만들기", + "yankVersion": "현재 버전 배포 철회", + "yanked": "스킬 모음 버전의 배포를 철회했습니다", + "yankConfirmTitle": "스킬 모음 버전 배포 철회", + "yankConfirmDescription": "v{{version}}의 배포를 철회하면 새 사용자가 설치할 수 없습니다. 구성 스킬에는 영향을 주지 않습니다.", + "yankReasonPlaceholder": "이 버전의 배포 철회 사유", + "hide": "스킬 모음 숨기기", + "restore": "숨김 해제", + "archive": "스킬 모음 보관", + "unarchive": "스킬 모음 복원", + "delete": "스킬 모음 삭제", + "deleted": "스킬 모음을 삭제했습니다", + "hideSuccess": "스킬 모음을 숨겼습니다", + "restoreSuccess": "스킬 모음 숨김을 해제했습니다", + "archiveSuccess": "스킬 모음을 보관했습니다", + "unarchiveSuccess": "스킬 모음을 복원했습니다", + "hideConfirmTitle": "스킬 모음 숨기기", + "hideConfirmDescription": "“{{name}}”이 일반 탐색 결과에 더 이상 표시되지 않습니다.", + "restoreConfirmTitle": "스킬 모음 숨김 해제", + "restoreConfirmDescription": "“{{name}}”을 공개 범위에 따라 다시 탐색할 수 있습니다.", + "archiveConfirmTitle": "스킬 모음 보관", + "archiveConfirmDescription": "“{{name}}”을 보관하면 새 버전을 만들 수 없습니다. 구성 스킬에는 영향을 주지 않습니다.", + "unarchiveConfirmTitle": "보관된 스킬 모음 복원", + "unarchiveConfirmDescription": "“{{name}}”을 복원하면 다시 새 버전을 만들 수 있습니다.", + "deleteConfirmTitle": "스킬 모음 삭제", + "deleteConfirmDescription": "구성 스킬은 삭제하지 않고 “{{name}}”과 해당 버전을 영구 삭제합니다. 이 작업은 되돌릴 수 없습니다.", + "deletePendingReviewHint": "검토 중인 스킬 모음은 삭제할 수 없습니다", + "blockingReasons": { + "DELETED": "삭제된 구성 스킬", + "NAMESPACE_ARCHIVED": "구성 스킬의 네임스페이스가 보관됨", + "NAMESPACE_FROZEN": "구성 스킬의 네임스페이스가 동결됨", + "SKILL_HIDDEN": "스킬 숨김", + "SKILL_ARCHIVED": "스킬을 보관했습니다", + "VERSION_UNAVAILABLE": "버전 사용 불가", + "VISIBILITY_INCOMPATIBLE": "공개 범위 호환 불가" + } + }, + "skillAvatar": { + "submittedNotice": "아바타 변경을 제출했습니다. 내 스킬 → 업데이트에서 검토 상태를 확인하세요.", + "removedNotice": "아바타 제거를 제출했습니다. 내 스킬 → 업데이트에서 검토 상태를 확인하세요.", + "detailHelp": "아바타도 이 신청과 함께 검토합니다. 변경, 이의 제기, 재시도는 내 스킬에서 업데이트를 선택하세요.", + "optionalTitle": "스킬 아바타(선택 사항)", + "stagedHelp": "2 MB 미만의 PNG 또는 JPEG를 선택하여 자른 후 업로드하세요. 제출하면 아바타와 스킬 패키지에 동일한 검토 결정이 적용됩니다.", + "cropUpload": "자른 이미지 업로드", + "uploadRetry": "업로드 재시도", + "discardChange": "아바타 변경 취소", + "saveChanges": "아바타 변경 저장", + "manage": "아바타 관리", + "uploadStates": { + "KEEP": "현재 아바타가 유지됩니다. 아바타가 없으면 첫 글자를 사용합니다.", + "SELECTED": "제출하기 전에 자르고 업로드하세요.", + "UPLOADING": "이미지를 업로드하는 중…", + "READY": "비공개로 업로드했습니다. 제출하면 적용 절차와 아바타 검토가 시작됩니다.", + "ERROR": "업로드에 실패했습니다. 제출 전에 재시도하거나 변경을 취소하세요.", + "REMOVE": "제출에 성공하면 아바타가 제거됩니다." + }, + "title": "스킬 아바타", + "ownerHelp": "이미지를 업로드하여 정사각형으로 자르세요. 새 아바타가 승인될 때까지 현재 아바타가 표시됩니다.", + "status": "상태", + "candidatePreview": "후보 아바타", + "choose": "아바타 이미지 선택", + "cropPreview": "자르기 미리보기", + "cropArea": "자르기 영역", + "cropHelp": "선택 영역이나 조절점을 끌어서 조정하세요. 선택 영역에 초점을 맞추고 방향키로 이동할 수 있습니다.", + "cropHandles": { + "nw": "왼쪽 위 크기 조절", + "n": "위쪽 크기 조절", + "ne": "오른쪽 위 크기 조절", + "e": "오른쪽 크기 조절", + "se": "오른쪽 아래 크기 조절", + "s": "아래쪽 크기 조절", + "sw": "왼쪽 아래 크기 조절", + "w": "왼쪽 크기 조절" + }, + "submit": "아바타 제출", + "remove": "현재 아바타 제거", + "appeal": "검토 요청", + "retry": "자동 검토 재시도", + "retryAfter": "자동 검토에 실패했습니다. {{time}} 이후에 다시 시도할 수 있습니다.", + "automaticRetryStopped": "자동 재시도가 중단되었습니다. 대기 시간이 지난 후 직접 재시도하거나 새 아바타를 제출하세요.", + "failed": "작업에 실패했습니다. 다시 시도하세요.", + "invalid": "2 MB 미만의 PNG 또는 JPEG 이미지를 선택하세요.", + "reviewTitle": "아바타 검토", + "loading": "불러오는 중", + "emptyQueue": "검토 대기 중인 아바타 없음", + "reason": "검토 사유(반려 시 필수)", + "approve": "승인", + "reject": "반려", + "settingsTitle": "아바타 검토 설정", + "settingsHelp": "자동 검토 제공자는 배포 시 설정합니다. 이 스위치로 자동 검토와 수동 검토를 제어합니다.", + "machineEnabled": "자동 검토 활성화", + "manualEnabled": "수동 검토 활성화", + "noReviewRisk": "두 검토 모두 꺼져 있습니다. 새 아바타가 즉시 표시됩니다. 저장하기 전에 위험을 확인하세요.", + "save": "설정 저장", + "saved": "저장됨", + "states": { + "PENDING": "대기 중", + "ACTIVE": "활성", + "REJECTED": "반려됨", + "SUPERSEDED": "대체됨", + "REMOVED": "제거됨" + }, + "machine": { + "SKIPPED": "자동 검토 꺼짐", + "PENDING": "자동 검토 대기", + "PROCESSING": "자동 검토 진행 중", + "PASSED": "자동 검토 통과", + "BLOCKED": "자동 검토 차단", + "ERROR": "자동 검토 실패" + }, + "cropTitle": "아바타 자르기", + "cropConfirm": "확인 및 업로드", + "change": "이미지 변경", + "stagedReady": "이미지 업로드 완료: 제출 준비됨", + "removedDraft": "대기 중인 변경: 아바타 제거", + "submittedImage": "제출한 아바타", + "publicUnchanged": "승인될 때까지 공개 아바타는 변경되지 않습니다.", + "candidateUnavailable": "제출한 아바타 미리보기 불가", + "statusLoadFailed": "제출한 아바타를 불러오지 못했습니다. 다시 시도하세요.", + "statusRefreshFailed": "검토 상태를 새로고침하지 못했습니다. 마지막으로 불러온 아바타가 표시됩니다.", + "reloadStatus": "다시 불러오기" + }, + "namespaceManagement": { + "back": "내 공간으로 돌아가기", + "enter": "공간으로 이동", + "navigation": "공간 관리", + "skills": "스킬", + "members": "멤버", + "reviews": "검토", + "settings": "설정", + "invite": "멤버 초대", + "lifecycle": "공간 수명 주기", + "globalReadOnly": "글로벌 공간은 동결, 보관, 삭제할 수 없습니다. 스킬 검토는 계속 사용할 수 있습니다.", + "lifecycleHint": "상태를 변경하기 전에 멤버, 검토, 스킬 접근에 미치는 영향을 확인하세요.", + "freezeHint": "멤버 관리, 게시, 검토를 일시 중지합니다. 게시된 스킬은 계속 표시됩니다.", + "unfreezeHint": "멤버 관리, 게시, 검토를 재개합니다.", + "archiveHint": "공간과 해당 스킬에 대한 일반 접근을 중단합니다. 보관은 되돌릴 수 있습니다.", + "restoreHint": "보관된 공간을 복원합니다.", + "deleteHint": "공간을 영구 삭제합니다. 먼저 스킬과 멤버를 처리하세요." + }, + "skillVisibility": { + "pendingList": "요청한 공개 범위: {{visibility}} · 검토 대기 중. 현재 접근 범위는 유지됩니다.", + "title": "공개 범위", + "description": "스킬 식별자, 패키지 버전, 통계를 변경하지 않고 접근 범위를 변경하세요.", + "current": "현재 범위: {{visibility}}", + "PUBLIC": "공개", + "NAMESPACE_ONLY": "네임스페이스 멤버", + "PRIVATE": "비공개(소유자 및 권한이 있는 관리자)", + "expanding": "접근 범위를 넓히려면 검토가 필요합니다. 승인 전까지 현재 접근 범위가 유지됩니다. 내 검토 진행 상황에서 요청을 확인하거나 철회하세요.", + "narrowing": "접근 범위를 좁히면 즉시 적용됩니다. 기존 사용자가 상세 정보와 다운로드에 접근하지 못할 수 있습니다.", + "submit": "검토 요청", + "submitted": "공개 범위 변경 검토를 요청했습니다", + "saved": "공개 범위를 변경했습니다", + "failed": "업데이트 실패", + "scanPENDING": "보안 검사 진행 중입니다. 통과하기 전에는 승인할 수 없습니다.", + "scanPASSED": "보안 검사를 통과했습니다.", + "scanFAILED": "보안 검사를 통과하지 못했습니다. 현재 공개 범위는 유지됩니다. 철회 후 재시도하거나 수정한 버전을 업로드하세요.", + "pending": "공개 범위 변경 요청 대기 중: {{visibility}}. 내 검토 진행 상황에서 완료하거나 철회하세요." + }, + "relativeTime": { + "justNow": "방금", + "minutes": "{{count}}분 전", + "hours": "{{count}}시간 전", + "days": "{{count}}일 전" + }, + "notificationEvents": { + "REVIEW_SUBMITTED": { + "title": "스킬 검토 제출", + "description": "{{skillName}}{{versionSuffix}}의 검토를 요청했습니다." + }, + "REVIEW_APPROVED": { + "title": "스킬 검토 승인", + "description": "{{skillName}}{{versionSuffix}}의 검토가 승인되었습니다." + }, + "REVIEW_REJECTED": { + "title": "스킬 검토 반려", + "description": "{{skillName}}{{versionSuffix}}의 검토가 반려되었습니다." + }, + "PROMOTION_SUBMITTED": { + "title": "스킬 승격 제출", + "description": "{{skillName}}{{versionSuffix}}의 승격을 요청했습니다." + }, + "PROMOTION_APPROVED": { + "title": "스킬 승격 승인", + "description": "{{skillName}}{{versionSuffix}}의 승격이 승인되었습니다." + }, + "PROMOTION_REJECTED": { + "title": "스킬 승격 반려", + "description": "{{skillName}}{{versionSuffix}}의 승격이 반려되었습니다." + }, + "REPORT_SUBMITTED": { + "title": "스킬 신고 접수", + "description": "{{skillName}}에 새 신고가 접수되었습니다." + }, + "REPORT_RESOLVED": { + "title": "스킬 신고 처리 완료", + "description": "{{skillName}}에 대한 신고가 처리되었습니다." + }, + "SKILL_PUBLISHED": { + "title": "스킬 게시 완료", + "description": "{{skillName}}{{versionSuffix}}이(가) 게시되었습니다." + }, + "SUBSCRIPTION_NEW_VERSION": { + "title": "구독한 스킬 업데이트", + "description": "{{skillName}}{{versionSuffix}}의 새 버전이 게시되었습니다." + }, + "SUBSCRIPTION_VERSION_YANKED": { + "title": "구독한 스킬 버전 배포 철회", + "description": "{{skillName}}{{versionSuffix}} 버전의 배포가 철회되었습니다." + } + } +} diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 2d03b2d65..815940390 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -6,6 +6,7 @@ "evidence": "Доказательства" }, "common": { + "loading": "Загрузка...", "expand": "Развернуть подробности", "collapse": "Свернуть подробности" }, @@ -379,6 +380,7 @@ "enterpriseContinue": "Войти с рабочим аккаунтом", "switchToPersonal": "Использовать личный аккаунт", "agreementPrefix": "Входя в систему, вы соглашаетесь с", + "agreement": "Входя в систему, вы соглашаетесь с Условиями использования и Политикой конфиденциальности", "terms": "Условиями использования", "and": "и", "privacy": "Политикой конфиденциальности" @@ -2099,6 +2101,9 @@ "title": "Файлы" }, "footer": { + "license": "Лицензия", + "contributing": "Участие в проекте", + "changelog": "История изменений", "product": "Продукт", "marketplace": "Каталог скиллов", "publish": "Опубликовать скилл", @@ -3242,5 +3247,57 @@ "scanPASSED": "Проверка безопасности пройдена.", "scanFAILED": "Проверка безопасности не пройдена. Доступ не изменён. Отзовите заявку и повторите попытку или загрузите исправленную версию.", "pending": "Ожидающая заявка: {{visibility}}. Завершите или отзовите её в разделе проверок." + }, + "relativeTime": { + "justNow": "только что", + "minutes": "{{count}} мин", + "hours": "{{count}} ч", + "days": "{{count}} д" + }, + "notificationEvents": { + "REVIEW_SUBMITTED": { + "title": "Проверка отправлена", + "description": "{{skillName}}{{versionSuffix}} отправлен на проверку." + }, + "REVIEW_APPROVED": { + "title": "Проверка одобрена", + "description": "{{skillName}}{{versionSuffix}} одобрен." + }, + "REVIEW_REJECTED": { + "title": "Проверка отклонена", + "description": "{{skillName}}{{versionSuffix}} отклонён." + }, + "PROMOTION_SUBMITTED": { + "title": "Продвижение отправлено", + "description": "{{skillName}}{{versionSuffix}} отправлен на продвижение." + }, + "PROMOTION_APPROVED": { + "title": "Продвижение одобрено", + "description": "Продвижение {{skillName}}{{versionSuffix}} одобрено." + }, + "PROMOTION_REJECTED": { + "title": "Продвижение отклонено", + "description": "Продвижение {{skillName}}{{versionSuffix}} отклонено." + }, + "REPORT_SUBMITTED": { + "title": "Жалоба на навык отправлена", + "description": "Получена новая жалоба на {{skillName}}." + }, + "REPORT_RESOLVED": { + "title": "Жалоба обработана", + "description": "Жалоба на {{skillName}} обработана." + }, + "SKILL_PUBLISHED": { + "title": "Навык опубликован", + "description": "{{skillName}}{{versionSuffix}} опубликован." + }, + "SUBSCRIPTION_NEW_VERSION": { + "title": "Обновление подписанного навыка", + "description": "Опубликована новая версия {{skillName}}{{versionSuffix}}." + }, + "SUBSCRIPTION_VERSION_YANKED": { + "title": "Версия подписанного навыка отозвана", + "description": "Версия {{skillName}}{{versionSuffix}} отозвана." + } } } diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index ca079e3fd..bd77fbe4d 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -6,6 +6,7 @@ "evidence": "证据" }, "common": { + "loading": "加载中...", "expand": "展开详情", "collapse": "收起详情" }, @@ -379,6 +380,7 @@ "enterpriseContinue": "通过企业账号登录", "switchToPersonal": "改用个人账号", "agreementPrefix": "登录即表示你同意我们的", + "agreement": "登录即表示你同意我们的服务条款和隐私政策", "terms": "服务条款", "and": "和", "privacy": "隐私政策" @@ -2207,6 +2209,9 @@ } }, "footer": { + "license": "许可证", + "contributing": "参与贡献", + "changelog": "更新日志", "product": "产品", "marketplace": "技能市场", "publish": "发布技能", @@ -3239,5 +3244,57 @@ "scanPASSED": "安全扫描已通过。", "scanFAILED": "安全扫描未通过,当前范围保持不变。请撤回后重试或上传修复版本。", "pending": "已有待审核范围申请:{{visibility}}。请到“我的审核进度”完成或撤回。" + }, + "relativeTime": { + "justNow": "刚刚", + "minutes": "{{count}}分钟", + "hours": "{{count}}小时", + "days": "{{count}}天" + }, + "notificationEvents": { + "REVIEW_SUBMITTED": { + "title": "技能审核提交", + "description": "{{skillName}}{{versionSuffix}} 已提交审核。" + }, + "REVIEW_APPROVED": { + "title": "技能审核通过", + "description": "{{skillName}}{{versionSuffix}} 已审核通过。" + }, + "REVIEW_REJECTED": { + "title": "技能审核驳回", + "description": "{{skillName}}{{versionSuffix}} 审核未通过。" + }, + "PROMOTION_SUBMITTED": { + "title": "技能推广提交", + "description": "{{skillName}}{{versionSuffix}} 已提交推广。" + }, + "PROMOTION_APPROVED": { + "title": "技能推广通过", + "description": "{{skillName}}{{versionSuffix}} 推广已通过。" + }, + "PROMOTION_REJECTED": { + "title": "技能推广驳回", + "description": "{{skillName}}{{versionSuffix}} 推广未通过。" + }, + "REPORT_SUBMITTED": { + "title": "技能举报提交", + "description": "{{skillName}} 收到新的举报。" + }, + "REPORT_RESOLVED": { + "title": "技能举报已处理", + "description": "{{skillName}} 的举报已处理。" + }, + "SKILL_PUBLISHED": { + "title": "技能发布成功", + "description": "{{skillName}}{{versionSuffix}} 已发布。" + }, + "SUBSCRIPTION_NEW_VERSION": { + "title": "订阅技能更新", + "description": "{{skillName}}{{versionSuffix}} 发布了新版本。" + }, + "SUBSCRIPTION_VERSION_YANKED": { + "title": "订阅技能版本撤回", + "description": "{{skillName}}{{versionSuffix}} 版本已撤回。" + } } } diff --git a/web/src/i18n/security-audit-locale.test.ts b/web/src/i18n/security-audit-locale.test.ts index 0ce69811e..cde8ef55f 100644 --- a/web/src/i18n/security-audit-locale.test.ts +++ b/web/src/i18n/security-audit-locale.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import en from './locales/en.json' +import ko from './locales/ko.json' import zh from './locales/zh.json' import ru from './locales/ru.json' @@ -15,6 +16,7 @@ describe('security audit locales', () => { }) it('uses the precise safe verdict wording in all locales', () => { + expect(ko.securityAudit.verdict.SAFE).toBe('고위험 항목이 발견되지 않음') expect(en.securityAudit.verdict.SAFE).toBe('No high-risk findings') expect(zh.securityAudit.verdict.SAFE).toBe('未发现高风险问题') expect(ru.securityAudit.verdict.SAFE).toBe('Угроз высокого риска не обнаружено') @@ -27,5 +29,6 @@ describe('security audit locales', () => { expect(Object.keys(en.securityAudit.verdict).sort()).toEqual( Object.keys(ru.securityAudit.verdict).sort(), ) + expect(Object.keys(ko.securityAudit.verdict).sort()).toEqual(Object.keys(en.securityAudit.verdict).sort()) }) }) diff --git a/web/src/i18n/suite-locale.test.ts b/web/src/i18n/suite-locale.test.ts index 1d078d604..de0a11f64 100644 --- a/web/src/i18n/suite-locale.test.ts +++ b/web/src/i18n/suite-locale.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import en from './locales/en.json' +import ko from './locales/ko.json' import ru from './locales/ru.json' import zh from './locales/zh.json' @@ -11,5 +12,7 @@ describe('suite locales', () => { expect(en.nav.suites).toBeTruthy() expect(ru.nav.suites).toBeTruthy() expect(zh.nav.suites).toBeTruthy() + expect(Object.keys(ko.suite).sort()).toEqual(expected) + expect(ko.nav.suites).toBeTruthy() }) }) diff --git a/web/src/index.css b/web/src/index.css index 7b243c4e3..68bb85e5a 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -167,6 +167,12 @@ text-rendering: optimizeLegibility; } + /* Keep Korean words together while allowing long identifiers to wrap. */ + html:lang(ko) body { + word-break: keep-all; + overflow-wrap: anywhere; + } + h1, h2, h3, h4, h5, h6 { font-family: var(--font-sans); font-weight: 500; diff --git a/web/src/pages/login.test.tsx b/web/src/pages/login.test.tsx index 40a7eabb6..aa69f87c8 100644 --- a/web/src/pages/login.test.tsx +++ b/web/src/pages/login.test.tsx @@ -1,9 +1,14 @@ /** @vitest-environment jsdom */ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { createInstance } from 'i18next' +import { I18nextProvider } from 'react-i18next' +import ko from '@/i18n/locales/ko.json' import { afterEach } from 'vitest' import { describe, expect, it, vi } from 'vitest' const authMethodsFixture = vi.hoisted(() => ({ + localize: false, + reason: undefined as string | undefined, methods: [] as Array<{ id: string, methodType: string }>, bootstrapEnabled: false, directEnabled: false, @@ -15,10 +20,12 @@ const authMethodsFixture = vi.hoisted(() => ({ registrationAvailable: true, })) +const koreanI18n = createInstance() + vi.mock('@tanstack/react-router', () => ({ - Link: ({ children }: { children: unknown }) => children, + Link: ({ children, to }: { children: React.ReactNode; to: string }) => {children}, useNavigate: () => authMethodsFixture.navigate, - useSearch: () => ({ returnTo: authMethodsFixture.returnTo }), + useSearch: () => ({ returnTo: authMethodsFixture.returnTo, reason: authMethodsFixture.reason }), })) vi.mock('react-i18next', async () => { @@ -26,7 +33,7 @@ vi.mock('react-i18next', async () => { return { ...actual, useTranslation: () => ({ - t: (key: string) => key, + t: (key: string) => authMethodsFixture.localize ? koreanI18n.t(key) : key, i18n: { resolvedLanguage: 'en' }, }), } @@ -95,6 +102,8 @@ import { LoginPage } from './login' describe('LoginPage', () => { afterEach(() => { cleanup() + authMethodsFixture.localize = false + authMethodsFixture.reason = undefined authMethodsFixture.methods = [] authMethodsFixture.bootstrapEnabled = false authMethodsFixture.directEnabled = false @@ -120,6 +129,25 @@ describe('LoginPage', () => { expect(html).toContain('login.register') }) + it('renders a complete Korean agreement with working policy links', async () => { + await koreanI18n.init({ lng: 'ko', resources: { ko: { translation: ko } } }) + authMethodsFixture.localize = true + render() + const terms = screen.getByRole('link', { name: '이용약관' }) + const privacy = screen.getByRole('link', { name: '개인정보처리방침' }) + expect(terms.getAttribute('href')).toBe('/terms') + expect(privacy.getAttribute('href')).toBe('/privacy') + expect(terms.parentElement?.textContent).toBe('로그인하면 이용약관 및 개인정보처리방침에 동의하게 됩니다.') + }) + + it('shows a translated disabled-account reason instead of a missing key', async () => { + await koreanI18n.init({ lng: 'ko', resources: { ko: { translation: ko } } }) + authMethodsFixture.localize = true + authMethodsFixture.reason = 'accountDisabled' + render() + expect(screen.getByText('비활성화된 계정입니다')).toBeTruthy() + }) + it('hides password and registration entry when local authentication is disabled', () => { authMethodsFixture.passwordEnabled = false authMethodsFixture.registrationAvailable = false diff --git a/web/src/pages/login.tsx b/web/src/pages/login.tsx index 369e81017..a6aaa6e13 100644 --- a/web/src/pages/login.tsx +++ b/web/src/pages/login.tsx @@ -1,6 +1,6 @@ import { Link, useNavigate, useSearch } from '@tanstack/react-router' import { useState } from 'react' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { ArrowRight, Eye, EyeOff, LockKeyhole, UserRound } from 'lucide-react' import { getSessionBootstrapRuntimeConfig } from '@/api/client' import { AuthShell } from '@/features/auth/auth-shell' @@ -20,7 +20,7 @@ import { resolveAuthReturnTo } from '@/shared/lib/auth-route' * preserving the route the user originally intended to visit. */ export function LoginPage() { - const { t, i18n } = useTranslation() + const { t } = useTranslation() const navigate = useNavigate() const search = useSearch({ from: '/login' }) const loginMutation = usePasswordLogin() @@ -30,13 +30,12 @@ export function LoginPage() { const [showPassword, setShowPassword] = useState(false) const [loginMode, setLoginMode] = useState<'personal' | 'organization'>('personal') const [fieldErrors, setFieldErrors] = useState<{ username?: string, password?: string }>({}) - const isChinese = i18n.resolvedLanguage?.split('-')[0] === 'zh' const returnTo = resolveAuthReturnTo(search.returnTo) const { data: authMethods, isLoading: authMethodsLoading } = useAuthMethods(returnTo) const { data: localCapabilities, isError: localCapabilitiesError } = useLocalAuthCapabilities() const passwordEnabled = localCapabilities?.passwordLoginEnabled === true const effectiveLoginMode = passwordEnabled ? loginMode : 'organization' - const disabledMessage = search.reason === 'accountDisabled' ? t('apiError.auth.accountDisabled') : null + const disabledMessage = search.reason === 'accountDisabled' ? t('error.auth.local.accountDisabled') : null const bootstrapMethod = authMethods?.find((method) => method.methodType === 'SESSION_BOOTSTRAP') const hasOrganizationMethod = bootstrapConfig.enabled const hasExternalMethods = authMethods?.some((method) => method.methodType === 'OAUTH_REDIRECT') @@ -203,17 +202,14 @@ export function LoginPage() {

: null}

- {t('login.agreementPrefix')} - {isChinese ? null : ' '} - - {t('login.terms')} - - {isChinese ? null : ' '} - {t('login.and')} - {isChinese ? null : ' '} - - {t('login.privacy')} - + , + privacy: , + }} + />

diff --git a/web/src/pages/privacy.test.tsx b/web/src/pages/privacy.test.tsx index 79183bf27..77eb38514 100644 --- a/web/src/pages/privacy.test.tsx +++ b/web/src/pages/privacy.test.tsx @@ -1,33 +1,43 @@ import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' +const mockI18n = vi.hoisted(() => ({ resolvedLanguage: 'en' })) + vi.mock('react-i18next', async () => { const actual = await vi.importActual('react-i18next') return { ...actual, useTranslation: () => ({ t: (key: string) => key, - i18n: { resolvedLanguage: 'en' }, + i18n: mockI18n, }), } }) -vi.mock('@/shared/components/legal-document', () => ({ - LegalDocument: (props: { title: string; summary: string }) => ( -
-

{props.title}

-

{props.summary}

-
- ), -})) - import { PrivacyPolicyPage } from './privacy' describe('PrivacyPolicyPage', () => { - it('renders the english privacy policy for non-chinese locales', () => { + it('renders the english privacy policy for the English locale', () => { + mockI18n.resolvedLanguage = 'en' const html = renderToStaticMarkup() expect(html).toContain('Privacy Policy') expect(html).toContain('This policy explains') }) + + it.each(['ko', 'ko-KR'])('renders the complete Korean policy for %s', (language) => { + mockI18n.resolvedLanguage = language + const html = renderToStaticMarkup() + expect(html).toContain('개인정보처리방침') + expect(html).toContain('8. 방침 업데이트 및 문의') + expect(html).toContain('2026년 3월 14일') + mockI18n.resolvedLanguage = 'en' + const english = renderToStaticMarkup() + expect((html.match(/
  • /g) ?? []).length).toBe((english.match(/
  • /g) ?? []).length) + }) + + it('preserves the English fallback for other languages', () => { + mockI18n.resolvedLanguage = 'ru' + expect(renderToStaticMarkup()).toContain('Privacy Policy') + }) }) diff --git a/web/src/pages/privacy.tsx b/web/src/pages/privacy.tsx index d37364a42..0e865e422 100644 --- a/web/src/pages/privacy.tsx +++ b/web/src/pages/privacy.tsx @@ -1,7 +1,9 @@ import { useTranslation } from 'react-i18next' +import koLegal from '@/i18n/locales/ko-legal.json' import { LegalDocument } from '@/shared/components/legal-document' const privacyDocuments = { + ko: koLegal.privacy, zh: { eyebrow: '法律与隐私', title: '隐私政策', @@ -152,7 +154,8 @@ const privacyDocuments = { export function PrivacyPolicyPage() { const { i18n } = useTranslation() - const language = i18n.resolvedLanguage?.split('-')[0] === 'zh' ? 'zh' : 'en' + const primaryLanguage = i18n.resolvedLanguage?.split('-')[0] + const language = primaryLanguage === 'zh' || primaryLanguage === 'ko' ? primaryLanguage : 'en' return } diff --git a/web/src/pages/terms.test.tsx b/web/src/pages/terms.test.tsx index d389a504e..8ffe3277a 100644 --- a/web/src/pages/terms.test.tsx +++ b/web/src/pages/terms.test.tsx @@ -1,33 +1,43 @@ import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' +const mockI18n = vi.hoisted(() => ({ resolvedLanguage: 'en' })) + vi.mock('react-i18next', async () => { const actual = await vi.importActual('react-i18next') return { ...actual, useTranslation: () => ({ t: (key: string) => key, - i18n: { resolvedLanguage: 'en' }, + i18n: mockI18n, }), } }) -vi.mock('@/shared/components/legal-document', () => ({ - LegalDocument: (props: { title: string; summary: string }) => ( -
    -

    {props.title}

    -

    {props.summary}

    -
    - ), -})) - import { TermsOfServicePage } from './terms' describe('TermsOfServicePage', () => { - it('renders the english terms of service for non-chinese locales', () => { + it('renders the english terms of service for the English locale', () => { + mockI18n.resolvedLanguage = 'en' const html = renderToStaticMarkup() expect(html).toContain('Terms of Service') expect(html).toContain('These terms apply') }) + + it.each(['ko', 'ko-KR'])('renders the complete Korean policy for %s', (language) => { + mockI18n.resolvedLanguage = language + const html = renderToStaticMarkup() + expect(html).toContain('이용약관') + expect(html).toContain('9. 종료, 변경 및 문의') + expect(html).toContain('2026년 3월 14일') + mockI18n.resolvedLanguage = 'en' + const english = renderToStaticMarkup() + expect((html.match(/
  • /g) ?? []).length).toBe((english.match(/
  • /g) ?? []).length) + }) + + it('preserves the English fallback for other languages', () => { + mockI18n.resolvedLanguage = 'ru' + expect(renderToStaticMarkup()).toContain('Terms of Service') + }) }) diff --git a/web/src/pages/terms.tsx b/web/src/pages/terms.tsx index 368fa486f..8fa462a53 100644 --- a/web/src/pages/terms.tsx +++ b/web/src/pages/terms.tsx @@ -1,7 +1,9 @@ import { useTranslation } from 'react-i18next' +import koLegal from '@/i18n/locales/ko-legal.json' import { LegalDocument } from '@/shared/components/legal-document' const termsDocuments = { + ko: koLegal.terms, zh: { eyebrow: '法律与政策', title: '服务条款', @@ -168,7 +170,8 @@ const termsDocuments = { export function TermsOfServicePage() { const { i18n } = useTranslation() - const language = i18n.resolvedLanguage?.split('-')[0] === 'zh' ? 'zh' : 'en' + const primaryLanguage = i18n.resolvedLanguage?.split('-')[0] + const language = primaryLanguage === 'zh' || primaryLanguage === 'ko' ? primaryLanguage : 'en' return } diff --git a/web/src/shared/components/language-switcher.test.tsx b/web/src/shared/components/language-switcher.test.tsx index 908625926..793bec909 100644 --- a/web/src/shared/components/language-switcher.test.tsx +++ b/web/src/shared/components/language-switcher.test.tsx @@ -49,6 +49,23 @@ describe('LanguageSwitcher', () => { expect(screen.getByRole('button', { name: /English/i })).toBeTruthy() }) + it('selects Korean and closes the menu', () => { + mockI18n.language = 'en' + mockI18n.resolvedLanguage = 'en' + render() + fireEvent.click(screen.getByRole('button', { name: /English/i })) + fireEvent.click(screen.getByRole('menuitem', { name: '한국어' })) + expect(mockI18n.changeLanguage).toHaveBeenCalledWith('ko') + expect(screen.getByRole('button', { name: /한국어/ })).toBeTruthy() + expect(screen.queryByRole('menu')).toBeNull() + }) + + it('shows Korean for the regional locale ko-KR', () => { + mockI18n.language = 'ko-KR' + render() + expect(screen.getByRole('button', { name: /한국어/ })).toBeTruthy() + }) + it('updates the displayed language after selecting English', () => { mockI18n.language = 'zh' mockI18n.resolvedLanguage = 'zh' diff --git a/web/src/shared/components/language-switcher.tsx b/web/src/shared/components/language-switcher.tsx index 1a298eecb..2e3e269ad 100644 --- a/web/src/shared/components/language-switcher.tsx +++ b/web/src/shared/components/language-switcher.tsx @@ -24,6 +24,7 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) { const languages = [ { code: 'zh', name: '中文' }, { code: 'en', name: 'English' }, + { code: 'ko', name: '한국어' }, { code: 'ru', name: 'Русский' }, ] diff --git a/web/src/shared/lib/api-error.test.ts b/web/src/shared/lib/api-error.test.ts index 5144fdf67..f39ad0e3e 100644 --- a/web/src/shared/lib/api-error.test.ts +++ b/web/src/shared/lib/api-error.test.ts @@ -49,6 +49,21 @@ describe('handleApiError', () => { expect(window.location.href).toBe('/login?reason=accountDisabled') }) + it('recognizes a Korean disabled-account message after switching to English', async () => { + const { ApiError, handleApiError } = await import('./api-error') + await i18n.changeLanguage('en') + handleApiError(new ApiError('비활성화된 계정입니다', 401, '비활성화된 계정입니다')) + expect(errorSpy).not.toHaveBeenCalled() + expect(window.location.href).toBe('/login?reason=accountDisabled') + }) + + it('shows localized network errors in Korean', async () => { + const { ApiError, handleApiError } = await import('./api-error') + await i18n.changeLanguage('ko') + handleApiError(new ApiError('Network error', 0)) + expect(errorSpy).toHaveBeenLastCalledWith('네트워크 연결에 실패했습니다. 네트워크를 확인하세요') + }) + it('falls back to the server message for non-standard api errors', async () => { const { ApiError, handleApiError } = await import('./api-error') diff --git a/web/src/shared/lib/api-error.ts b/web/src/shared/lib/api-error.ts index 2a75e7196..a9c096814 100644 --- a/web/src/shared/lib/api-error.ts +++ b/web/src/shared/lib/api-error.ts @@ -28,10 +28,11 @@ export class ApiError extends Error { function isAccountDisabledError(error: ApiError): boolean { const accountDisabledMessages = [ - i18n.t('apiError.auth.accountDisabled'), - i18n.getFixedT('en')('apiError.auth.accountDisabled'), - i18n.getFixedT('ru')('apiError.auth.accountDisabled'), - i18n.getFixedT('zh')('apiError.auth.accountDisabled'), + i18n.t('error.auth.local.accountDisabled'), + i18n.getFixedT('en')('error.auth.local.accountDisabled'), + i18n.getFixedT('ko')('error.auth.local.accountDisabled'), + i18n.getFixedT('ru')('error.auth.local.accountDisabled'), + i18n.getFixedT('zh')('error.auth.local.accountDisabled'), ] const normalizedServerMessage = (error.serverMessage ?? '').toLowerCase() const normalizedMessage = error.message.toLowerCase() diff --git a/web/src/shared/lib/document-language.test.ts b/web/src/shared/lib/document-language.test.ts index 744ab6a7f..705df3bc3 100644 --- a/web/src/shared/lib/document-language.test.ts +++ b/web/src/shared/lib/document-language.test.ts @@ -20,6 +20,15 @@ describe('syncDocumentLanguage', () => { expect(document.getElementById('root')?.getAttribute('translate')).toBe('no') }) + it('updates the document language when switching from Korean back to English', () => { + document.body.innerHTML = '
    ' + syncDocumentLanguage('ko-KR') + expect(document.documentElement.lang).toBe('ko') + syncDocumentLanguage('en') + expect(document.documentElement.lang).toBe('en') + expect(document.getElementById('root')?.getAttribute('translate')).toBe('no') + }) + it('uses the primary subtag for other locales', () => { document.body.innerHTML = '
    ' syncDocumentLanguage('ru-RU') diff --git a/web/src/shared/lib/format-relative-time.test.ts b/web/src/shared/lib/format-relative-time.test.ts new file mode 100644 index 000000000..70877ec68 --- /dev/null +++ b/web/src/shared/lib/format-relative-time.test.ts @@ -0,0 +1,35 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import i18n from '@/i18n/config' +import { formatRelativeTime } from './format-relative-time' + +const now = new Date('2026-06-15T12:00:00Z') +const ago = (minutes: number) => new Date(now.getTime() - minutes * 60_000).toISOString() + +describe('formatRelativeTime', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(now) + }) + afterEach(() => vi.useRealTimers()) + + it.each([ + [0, '방금'], [0.5, '방금'], [1, '1분 전'], [59, '59분 전'], + [60, '1시간 전'], [1439, '23시간 전'], [1440, '1일 전'], [29 * 1440, '29일 전'], + ])('formats %s minutes ago in Korean', (minutes, expected) => { + expect(formatRelativeTime(ago(minutes), 'ko-KR')).toBe(expected) + }) + + it.each([['en', '5m'], ['zh-CN', '5分钟'], ['ru-RU', '5 мин']])('preserves compact %s output', (locale, expected) => { + expect(formatRelativeTime(ago(5), locale)).toBe(expected) + }) + + it('uses the selected locale for older dates', () => { + const date = ago(30 * 1440) + expect(formatRelativeTime(date, 'ko')).toBe(new Date(date).toLocaleDateString('ko')) + }) + + it.each([0, 5, 60, 1440, 30 * 1440])('does not append a second suffix in Korean notifications (%s minutes)', (minutes) => { + const time = formatRelativeTime(ago(minutes), 'ko') + expect(i18n.getFixedT('ko')('notification.timeAgo', { time })).toBe(time) + }) +}) diff --git a/web/src/shared/lib/format-relative-time.ts b/web/src/shared/lib/format-relative-time.ts index b0f022772..d78cf5312 100644 --- a/web/src/shared/lib/format-relative-time.ts +++ b/web/src/shared/lib/format-relative-time.ts @@ -1,34 +1,16 @@ -/** - * Formats a timestamp as a compact relative time string for notification UI. - * Mirrors the zh inline pattern; ru support added for the Russian locale. - */ +import i18n from '@/i18n/config' + +/** Formats a timestamp for skill cards and notification UI in the selected language. */ export function formatRelativeTime(dateStr: string, lang: string): string { const diff = Date.now() - new Date(dateStr).getTime() const minutes = Math.floor(diff / 60_000) const hours = Math.floor(diff / 3_600_000) const days = Math.floor(diff / 86_400_000) - const isChinese = lang.startsWith('zh') - const isRussian = lang.startsWith('ru') + const t = i18n.getFixedT(lang) - if (minutes < 1) { - if (isChinese) return '刚刚' - if (isRussian) return 'только что' - return 'just now' - } - if (minutes < 60) { - if (isChinese) return `${minutes}分钟` - if (isRussian) return `${minutes} мин` - return `${minutes}m` - } - if (hours < 24) { - if (isChinese) return `${hours}小时` - if (isRussian) return `${hours} ч` - return `${hours}h` - } - if (days < 30) { - if (isChinese) return `${days}天` - if (isRussian) return `${days} д` - return `${days}d` - } - return new Date(dateStr).toLocaleDateString(isRussian ? 'ru-RU' : undefined) + if (minutes < 1) return t('relativeTime.justNow') + if (minutes < 60) return t('relativeTime.minutes', { count: minutes }) + if (hours < 24) return t('relativeTime.hours', { count: hours }) + if (days < 30) return t('relativeTime.days', { count: days }) + return new Date(dateStr).toLocaleDateString(lang) }