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() {
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}}1>개",
+ "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)
}