artifacty 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib/i18n.js CHANGED
@@ -1,8 +1,317 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
1
3
  export const DEFAULT_LOCALE = "en";
2
4
  export const SUPPORTED_LOCALES = ["en", "ko"];
5
+ const languageContext = new AsyncLocalStorage();
6
+
7
+ // Settings belong to a server/store, not the process. Concurrent servers must
8
+ // never share a custom translation catalog or another administrator's choices.
9
+ export function runWithLanguageSettings(settings, callback) {
10
+ const packs = settings.packs || [];
11
+ return languageContext.run({
12
+ defaultLocale: settings.defaultLocale,
13
+ enabledLocales: [...settings.enabledLocales],
14
+ packs: packs.map((pack) => ({ ...pack, messages: { ...pack.messages } }))
15
+ }, callback);
16
+ }
17
+
18
+ export function defaultLocale() {
19
+ return languageContext.getStore()?.defaultLocale || DEFAULT_LOCALE;
20
+ }
3
21
 
4
- const TRANSLATIONS = {
22
+ export function enabledLocales() {
23
+ return [...(languageContext.getStore()?.enabledLocales || SUPPORTED_LOCALES)];
24
+ }
25
+
26
+ export function availableLanguages() {
27
+ const enabled = enabledLocales();
28
+ return [
29
+ { locale: "en", nativeName: "English" },
30
+ { locale: "ko", nativeName: "한국어" },
31
+ ...(languageContext.getStore()?.packs || []).map(({ locale, nativeName }) => ({ locale, nativeName }))
32
+ ].map((language) => ({ ...language, enabled: enabled.includes(language.locale) }));
33
+ }
34
+
35
+ export const TRANSLATIONS = {
5
36
  en: {
37
+ "nav.collapseSidebar": "Collapse sidebar",
38
+ "nav.expandSidebar": "Expand sidebar",
39
+ "apiDocs.title": "API reference",
40
+ "apiDocs.copy": "Copy",
41
+ "apiDocs.copied": "Copied to clipboard.",
42
+ "apiDocs.copyFailed": "Copy failed. Select and copy the example manually.",
43
+ "apiDocs.requestExample": "Example request",
44
+ "apiDocs.responseExample": "Example response",
45
+ "apiDocs.schema": "Schema",
46
+ "apiDocs.schemas": "Schemas",
47
+ "apiDocs.noSchema": "See the endpoint description for the response format.",
48
+ "apiDocs.access": "Required access",
49
+ "apiDocs.parameters": "Parameters",
50
+ "apiDocs.name": "Name",
51
+ "apiDocs.location": "Location",
52
+ "apiDocs.type": "Type",
53
+ "apiDocs.description": "Description",
54
+ "apiDocs.required": "required",
55
+ "apiDocs.responses": "Responses",
56
+ "apiDocs.download": "Download OpenAPI JSON",
57
+ "apiDocs.intro": "Browse endpoints, inspect schemas, and copy request examples.",
58
+ "apiDocs.search": "Find an endpoint",
59
+ "apiDocs.searchPlaceholder": "Search method, path, or description",
60
+ "apiDocs.noResults": "No matching endpoints.",
61
+ "apiDocs.sampleNote": "All examples are samples, not live data. Replace IDs and YOUR_API_TOKEN before use. Copying does not send a request. Write commands change data when run. Backup import uses your exported backup.json file.",
62
+ "dashboard.pagination": "Pagination",
63
+ "dashboard.pagePosition": "Page {page} of {pages}",
64
+ "dashboard.pageSize": "Per page",
65
+ "smtp.authHelp": "Leave the username blank to disable SMTP authentication and clear the saved password. With a username, leave the password blank to keep the saved password.",
66
+
67
+ "artifact.visibility.public": "Public",
68
+ "artifact.visibility.help": "Team requires sign-in. Public allows anonymous viewing only when the server is in public mode. Private server mode always requires sign-in and access permissions.",
69
+ "theme.label": "Appearance",
70
+ "theme.system": "System",
71
+ "theme.light": "Light",
72
+ "theme.dark": "Dark",
73
+
74
+ "smtp.invalidSettings": "Check the host, port, TLS mode, and sender address.",
75
+ "smtp.title": "Email / SMTP",
76
+ "smtp.settings": "Outgoing email settings",
77
+ "smtp.help": "Send administrator password resets through your SMTP server. Saving these settings does not send an email.",
78
+ "smtp.security": "The saved SMTP password is encrypted. TLS is required and server certificates are verified.",
79
+ "smtp.enabled": "Enable password reset emails",
80
+ "smtp.host": "SMTP host",
81
+ "smtp.port": "Port",
82
+ "smtp.tlsMode": "TLS connection",
83
+ "smtp.tls": "Implicit TLS",
84
+ "smtp.starttls": "STARTTLS (required)",
85
+ "smtp.from": "From email address",
86
+ "smtp.username": "Username",
87
+ "smtp.password": "SMTP password",
88
+ "smtp.keepPassword": "Leave blank to keep the saved password",
89
+ "smtp.enterPassword": "Enter SMTP password",
90
+ "smtp.restoreNote": "Keep the server encryption key with your recovery materials. Restored encrypted credentials need the same key.",
91
+ "smtp.save": "Save email settings",
92
+ "smtp.saved": "Email settings saved.",
93
+ "smtp.saveFailed": "Email settings could not be saved. Check the host, port, TLS mode, and sender address.",
94
+ "smtp.mailStatus.sent": "The SMTP server accepted the reset email. This does not confirm inbox delivery.",
95
+ "smtp.mailStatus.failed": "Email delivery could not be confirmed. Copy the temporary password now and share it securely with this user.",
96
+ "smtp.mailStatus.not-configured": "Email is not configured. Copy the temporary password now and share it securely with this user.",
97
+ "smtp.resetSubject": "Your Artifacty temporary password",
98
+ "smtp.resetBody": "Account: {email}\nTemporary password: {password}\nSign in to Artifacty and change this password.",
99
+
100
+ "users.resetPassword": "Reset password",
101
+ "users.resetFor": "Reset password for {email}",
102
+ "users.resetReady": "Temporary password created",
103
+ "users.resetAccount": "Account: {email}",
104
+ "users.resetHelp": "Copy this temporary password now. It is shown only once. Existing sessions have ended. The user must sign in with this password and change it before continuing.",
105
+
106
+ "access.previewTitle": "Access mode preview",
107
+ "access.backToSettings": "Back to access settings",
108
+ "access.previewAnonymous": "This preview shows what an anonymous visitor can see. It does not simulate a signed-in user or team.",
109
+ "access.previewUnsaved": "Settings have not been saved. Return to access settings to apply this mode. Artifact links are disabled in this preview.",
110
+ "access.previewListCount": "Showing {shown} of {total} artifacts visible to anonymous visitors.",
111
+ "access.previewSignIn": "Sign-in required",
112
+ "access.previewPrivate": "Anonymous visitors cannot open the dashboard in private mode. Signed-in users can view artifacts according to their assigned permissions.",
113
+
114
+ "users.lastAdmin": "At least one active administrator must remain.",
115
+ "users.roleFor": "Role for {email}",
116
+ "users.saveRoleFor": "Save role for {email}",
117
+ "users.saveRole": "Save",
118
+ "users.invalidRole": "Choose user or administrator.",
119
+
120
+ "languageAdmin.template": "Download translation template",
121
+ "languageAdmin.templateHelp": "Change the language code and native name, translate the messages, then paste the JSON below.",
122
+ "languageAdmin.tooLarge": "Language pack exceeds 512 KiB.",
123
+ "languageAdmin.invalidJson": "Enter a valid JSON language pack.",
124
+ "retention.artifactsSection": "Artifact lifecycle",
125
+ "retention.typeRulesSection": "Rules by artifact type",
126
+ "retention.historySection": "Audit and event history",
127
+
128
+ "language.label": "Language",
129
+ "language.apply": "Apply",
130
+ "languageAdmin.title": "Languages",
131
+ "languageAdmin.settings": "Language settings",
132
+ "languageAdmin.enabled": "Enabled languages",
133
+ "languageAdmin.default": "Default language",
134
+ "languageAdmin.defaultHelp": "The default language must be enabled. Visitors can choose another enabled language.",
135
+ "languageAdmin.save": "Save language settings",
136
+ "languageAdmin.import": "Import language pack",
137
+ "languageAdmin.pack": "Language pack JSON",
138
+ "languageAdmin.importHelp": "Paste a JSON object with locale, nativeName, and messages. The maximum size is 512 KiB. You can translate only some message keys; missing messages use English. Keep placeholders such as {count} unchanged.",
139
+ "languageAdmin.disabledHelp": "A newly imported language starts disabled. Enable it above when it is ready.",
140
+
141
+ "media.unavailable": "Media preview is unavailable because this artifact does not contain valid base64 media content.",
142
+ "media.image": "Image artifact",
143
+ "media.encoding": "encoding: {encoding}",
144
+ "media.source": "source: {encoding}",
145
+ "inbox.type.shared-with-me": "Shared with me",
146
+ "inbox.type.comment": "New comment",
147
+ "inbox.type.new-version": "New version",
148
+ "inbox.type.review-requested": "Review requested",
149
+
150
+ "auth.invalidCredentials": "Invalid email or password.",
151
+ "password.mismatch": "New password and confirmation do not match.",
152
+ "password.changed": "Password changed.",
153
+ "viewer.filter": "Filter",
154
+ "viewer.filterColumn": "Filter column {column}",
155
+ "viewer.ruleFilter": "Filter by rule id",
156
+ "viewer.rowsShown": "{visible} of {total} rows shown",
157
+ "viewer.resultsShown": "{visible} of {total} results shown",
158
+ "csv.empty": "Empty CSV artifact.",
159
+ "csv.error": "CSV parsing failed: {error}. Showing escaped source.",
160
+ "csv.column": "Column {column}",
161
+ "csv.rows": "{count} data rows",
162
+ "csv.columns": "{count} columns",
163
+ "csv.firstRows": "showing first {count} rows",
164
+ "csv.firstColumns": "showing first {count} columns",
165
+ "sarif.invalid": "Invalid SARIF JSON. Showing escaped source.",
166
+ "sarif.shape": "This JSON does not match the expected SARIF top-level shape. Showing formatted JSON.",
167
+ "sarif.first": "Showing first {count} of {total} results.",
168
+ "sarif.results": "{count} results.",
169
+ "sarif.runs": "Runs",
170
+ "sarif.resultsTitle": "Results",
171
+ "sarif.errors": "Errors",
172
+ "sarif.warnings": "Warnings",
173
+ "sarif.notes": "Notes",
174
+ "sarif.level": "Level",
175
+ "sarif.rule": "Rule",
176
+ "sarif.message": "Message",
177
+ "sarif.location": "Location",
178
+ "sarif.tool": "Tool",
179
+ "sarif.raw": "Raw SARIF JSON",
180
+ "react.disabled": "React rendering is disabled. Set ARTIFACTY_ENABLE_REACT_RENDERER=true to run this component in a sandboxed frame.",
181
+ "notebook.cells": "Showing the first {count} of {total} cells.",
182
+ "notebook.outputs": "Showing the first {count} of {total} outputs.",
183
+ "notebook.unsupported": "Unsupported output type: {type}",
184
+ "notebook.image": "Notebook output image",
185
+ "notebook.truncated": "Output truncated: {size} bytes exceeds the 2 MB limit.",
186
+
187
+ "versions.manage": "Manage versions",
188
+ "ui.version": "Version",
189
+ "ui.created": "Created",
190
+ "ui.bytes": "Bytes",
191
+ "ui.action": "Action",
192
+ "ui.reason": "Reason",
193
+ "ui.delete": "Delete",
194
+ "versions.view": "View version",
195
+ "versions.repair": "Repair version",
196
+ "ui.email": "Email",
197
+ "ui.name": "Name",
198
+ "ui.password": "Password",
199
+ "ui.revoke": "Revoke",
200
+ "account.newToken": "New API token",
201
+ "account.profile": "Profile",
202
+ "account.createToken": "Create token",
203
+ "account.tokens": "API tokens",
204
+ "ui.lastUsed": "Last used",
205
+ "ui.status": "Status",
206
+ "account.noTokens": "No API tokens.",
207
+ "ui.title": "Title",
208
+ "ui.updated": "Updated",
209
+ "ui.none": "None.",
210
+ "password.title": "Change Password",
211
+ "password.submit": "Change password",
212
+ "password.current": "Current password",
213
+ "password.new": "New password",
214
+ "password.confirm": "Confirm password",
215
+ "backup.status": "Store status",
216
+ "backup.home": "Home",
217
+ "backup.bytes": "Content bytes",
218
+ "backup.integrity": "Integrity",
219
+ "backup.download": "Download backup",
220
+ "backup.restore": "Restore backup",
221
+ "backup.json": "Backup JSON",
222
+ "users.importResults": "Import results",
223
+ "ui.role": "Role",
224
+ "users.resetRequired": "Reset required",
225
+ "users.temporaryPassword": "Temporary password",
226
+ "users.skipped": "Skipped",
227
+ "users.failed": "Failed",
228
+ "ui.row": "Row",
229
+ "ui.error": "Error",
230
+ "users.create": "Create user",
231
+ "users.importTitle": "Import users from CSV",
232
+ "users.import": "Import users",
233
+ "users.existing": "Existing users",
234
+ "import.generic": "Generic",
235
+ "login.setupHelp": "No users exist yet. The first account becomes an administrator.",
236
+ "account.copyToken": "Copy this token now. Artifacty stores only its hash and cannot show it again.",
237
+ "password.required": "Password change is required before continuing.",
238
+ "users.csvHelp": "Use headers: email, name, role, password, password_reset_required. Missing passwords are generated and must be changed at first sign-in.",
239
+ "login.setup": "Create admin account",
240
+ "login.create": "Create admin",
241
+ "ui.never": "Never",
242
+ "ui.active": "Active",
243
+ "ui.disabled": "Disabled",
244
+ "ui.disable": "Disable",
245
+ "ui.enable": "Enable",
246
+ "ui.yes": "Yes",
247
+ "ui.no": "No",
248
+ "users.provided": "Provided in CSV",
249
+ "users.requirePassword": "Require password change",
250
+ "account.tokenName": "Token name",
251
+ "backup.ok": "OK",
252
+ "backup.attention": "Needs attention",
253
+ "versions.content": "v{version} content",
254
+ "users.summary": "{created} created, {skipped} skipped, {failed} failed.",
255
+ "backup.complete": "Restore complete. {count} artifacts imported",
256
+ "ui.role.user": "user",
257
+ "ui.role.admin": "admin",
258
+ "backup.integrityHelp": "Run artifacty integrity before migration. Current backup will only include readable referenced version files.",
259
+ "backup.exportHelp": "Exports artifact metadata and version contents as one JSON bundle. The full scope also includes users, API tokens, audit history, relations, and webhooks (secrets are never included and must be re-issued after restore).",
260
+ "backup.restoreHelp": "Restore replaces the target server's artifact records and version files with the backup contents. An artifacts-only bundle leaves users, sessions, API tokens, and audit logs unchanged. A full-scope bundle also replaces users, API tokens, audit history, relations, and webhooks (disabled on import; they must be re-issued) — check both boxes below to proceed, and force-users only if this server already has accounts.",
261
+
262
+ "update.releaseNotes": "Release notes",
263
+ "update.checkFailed": "Could not check the latest version. Try checking again.",
264
+ "update.checkAgain": "Check again to prepare a new update confirmation.",
265
+ "update.available": "An update is available. Update to v{version}?",
266
+ "update.signIn": "Your session has expired or access was denied. Sign in again or refresh this page.",
267
+ "update.title": "Server update",
268
+ "update.installed": "Installed version",
269
+ "update.latest": "Available version",
270
+ "update.check": "Check for updates",
271
+ "update.help": "Updates from this page require an Artifacty Linux systemd user service. Check for updates to verify this installation.",
272
+ "update.backup": "The updater creates a backup before installing the selected version. The server briefly stops and restarts. Keep this page open to reconnect.",
273
+ "update.manual": "For other installations, use the update instructions for your installation method.",
274
+ "update.unsupported": "This installation cannot update from the browser.",
275
+ "update.current": "You’re up to date.",
276
+ "update.confirm": "I agree to back up the store and restart the server to install this version.",
277
+ "update.start": "Back up and update",
278
+ "update.progress": "Update status",
279
+ "update.refresh": "Refresh status",
280
+ "update.reconnect": "The server is restarting or unavailable. Reconnecting…",
281
+ "update.queued": "Waiting to start",
282
+ "update.running": "Update in progress",
283
+ "update.succeeded": "Update complete. Refresh this page to use the new version.",
284
+ "update.failed": "Update failed. Review the error before retrying.",
285
+ "update.interrupted": "The previous update was interrupted. Check the server before retrying.",
286
+ "update.expires": "Confirmation expires",
287
+
288
+ "artifact.details": "Details",
289
+ "nav.skip": "Skip to content",
290
+ "nav.artifacts": "Artifacts",
291
+ "nav.administration": "Administration",
292
+ "nav.users": "Users",
293
+ "nav.backup": "Backup",
294
+ "nav.webhooks": "Webhooks",
295
+ "nav.account": "Account",
296
+ "nav.accountTokens": "Account & API tokens",
297
+ "nav.password": "Password",
298
+ "nav.signIn": "Sign in",
299
+ "nav.signOut": "Sign out",
300
+ "nav.apiDocs": "API docs",
301
+ "nav.versions": "Versions",
302
+
303
+ "dashboard.navigation": "Dashboard navigation",
304
+ "dashboard.more": "Menu",
305
+ "dashboard.searchPlaceholder": "Search artifacts",
306
+ "dashboard.advanced": "Advanced filters",
307
+ "dashboard.activeFilters": "{count} active",
308
+ "filter.artifactType": "Artifact type",
309
+ "filter.reviewStatus": "Review status",
310
+ "filter.mode": "Search mode",
311
+ "filter.groupBy": "Group by",
312
+ "filter.relatedTo": "Related artifact ID",
313
+ "filter.relation": "Relation",
314
+
6
315
  "sharing.staticPreview": "Static preview; scripts and external resources are disabled.",
7
316
  "sharing.safeSource": "Source preview: active content is disabled for public share links.",
8
317
  "sharing.networkCondition": "All permissions also require an allowed connection IP",
@@ -278,6 +587,284 @@ const TRANSLATIONS = {
278
587
  "views.namePlaceholder": "View name"
279
588
  },
280
589
  ko: {
590
+ "nav.collapseSidebar": "사이드바 접기",
591
+ "nav.expandSidebar": "사이드바 펼치기",
592
+ "apiDocs.title": "API 문서",
593
+ "apiDocs.copy": "복사",
594
+ "apiDocs.copied": "클립보드에 복사했습니다.",
595
+ "apiDocs.copyFailed": "복사하지 못했습니다. 예제를 선택해 직접 복사해 주세요.",
596
+ "apiDocs.requestExample": "요청 예제",
597
+ "apiDocs.responseExample": "응답 예제",
598
+ "apiDocs.schema": "스키마",
599
+ "apiDocs.schemas": "스키마",
600
+ "apiDocs.noSchema": "응답 형식은 엔드포인트 설명을 확인하세요.",
601
+ "apiDocs.access": "필요 권한",
602
+ "apiDocs.parameters": "매개변수",
603
+ "apiDocs.name": "이름",
604
+ "apiDocs.location": "위치",
605
+ "apiDocs.type": "유형",
606
+ "apiDocs.description": "설명",
607
+ "apiDocs.required": "필수",
608
+ "apiDocs.responses": "응답",
609
+ "apiDocs.download": "OpenAPI JSON 다운로드",
610
+ "apiDocs.intro": "엔드포인트와 스키마를 살펴보고 요청 예제를 복사할 수 있습니다.",
611
+ "apiDocs.search": "엔드포인트 찾기",
612
+ "apiDocs.searchPlaceholder": "메서드, 경로 또는 설명 검색",
613
+ "apiDocs.noResults": "일치하는 엔드포인트가 없습니다.",
614
+ "apiDocs.sampleNote": "모든 예제는 실제 데이터가 아닌 샘플입니다. 사용 전에 ID와 YOUR_API_TOKEN을 바꾸세요. 복사만으로 요청이 전송되지는 않습니다. 쓰기 명령을 실행하면 데이터가 변경됩니다. 백업 가져오기에는 직접 내보낸 backup.json 파일을 사용하세요.",
615
+ "dashboard.pagination": "페이지 이동",
616
+ "dashboard.pagePosition": "{pages}페이지 중 {page}페이지",
617
+ "dashboard.pageSize": "페이지당 항목",
618
+ "smtp.authHelp": "사용자 이름을 비우면 SMTP 인증을 사용하지 않고 저장된 비밀번호도 삭제합니다. 사용자 이름을 입력한 경우 비밀번호를 비워 두면 저장된 비밀번호를 유지합니다.",
619
+
620
+ "artifact.visibility.public": "전체 공개",
621
+ "artifact.visibility.help": "팀 공개는 로그인이 필요합니다. 전체 공개는 서버가 퍼블릭 모드일 때만 익명 열람을 허용합니다. 서버가 프라이빗 모드이면 로그인과 접근 권한이 항상 필요합니다.",
622
+ "theme.label": "화면 테마",
623
+ "theme.system": "시스템",
624
+ "theme.light": "라이트",
625
+ "theme.dark": "다크",
626
+
627
+ "smtp.invalidSettings": "호스트, 포트, TLS 방식과 보내는 주소를 확인하세요.",
628
+ "smtp.title": "이메일 / SMTP",
629
+ "smtp.settings": "메일 발송 설정",
630
+ "smtp.help": "관리자가 초기화한 임시 비밀번호를 SMTP 서버로 발송합니다. 설정을 저장하는 것만으로 메일이 발송되지는 않습니다.",
631
+ "smtp.security": "저장된 SMTP 비밀번호는 암호화됩니다. TLS 연결이 필수이며 서버 인증서를 검증합니다.",
632
+ "smtp.enabled": "비밀번호 초기화 메일 발송 사용",
633
+ "smtp.host": "SMTP 호스트",
634
+ "smtp.port": "포트",
635
+ "smtp.tlsMode": "TLS 연결 방식",
636
+ "smtp.tls": "처음부터 TLS 연결",
637
+ "smtp.starttls": "STARTTLS (필수)",
638
+ "smtp.from": "보내는 이메일 주소",
639
+ "smtp.username": "사용자 이름",
640
+ "smtp.password": "SMTP 비밀번호",
641
+ "smtp.keepPassword": "비워 두면 저장된 비밀번호 유지",
642
+ "smtp.enterPassword": "SMTP 비밀번호 입력",
643
+ "smtp.restoreNote": "서버 암호화 키를 복구 자료와 함께 안전하게 보관하세요. 암호화된 인증 정보를 복원하려면 같은 키가 필요합니다.",
644
+ "smtp.save": "메일 설정 저장",
645
+ "smtp.saved": "메일 설정을 저장했습니다.",
646
+ "smtp.saveFailed": "메일 설정을 저장하지 못했습니다. 호스트, 포트, TLS 방식과 보내는 주소를 확인하세요.",
647
+ "smtp.mailStatus.sent": "SMTP 서버가 비밀번호 초기화 메일을 접수했습니다. 받은편지함 도착까지 확인된 것은 아닙니다.",
648
+ "smtp.mailStatus.failed": "메일 발송을 확인하지 못했습니다. 지금 임시 비밀번호를 복사해 해당 사용자에게 안전하게 전달하세요.",
649
+ "smtp.mailStatus.not-configured": "메일 발송이 설정되지 않았습니다. 지금 임시 비밀번호를 복사해 해당 사용자에게 안전하게 전달하세요.",
650
+ "smtp.resetSubject": "Artifacty 임시 비밀번호 안내",
651
+ "smtp.resetBody": "계정: {email}\n임시 비밀번호: {password}\nArtifacty에 로그인한 뒤 비밀번호를 변경하세요.",
652
+
653
+ "users.resetPassword": "비밀번호 초기화",
654
+ "users.resetFor": "{email}의 비밀번호 초기화",
655
+ "users.resetReady": "임시 비밀번호 생성됨",
656
+ "users.resetAccount": "계정: {email}",
657
+ "users.resetHelp": "지금 임시 비밀번호를 복사하세요. 이 화면에서 한 번만 표시됩니다. 기존 세션은 종료되었습니다. 사용자는 이 비밀번호로 로그인한 뒤 비밀번호를 변경해야 합니다.",
658
+
659
+ "access.previewTitle": "접근 모드 미리보기",
660
+ "access.backToSettings": "접근 설정으로 돌아가기",
661
+ "access.previewAnonymous": "이 화면은 로그인하지 않은 방문자에게 보이는 내용을 보여 줍니다. 로그인한 사용자나 팀의 권한을 재현하지 않습니다.",
662
+ "access.previewUnsaved": "설정은 저장되지 않았습니다. 이 모드를 적용하려면 접근 설정으로 돌아가세요. 미리보기에서는 아티팩트 링크가 비활성화됩니다.",
663
+ "access.previewListCount": "로그인하지 않은 방문자에게 보이는 아티팩트 {total}개 중 {shown}개를 표시합니다.",
664
+ "access.previewSignIn": "로그인 필요",
665
+ "access.previewPrivate": "프라이빗 모드에서는 로그인하지 않은 방문자가 대시보드를 열 수 없습니다. 로그인한 사용자는 부여된 권한에 따라 아티팩트를 볼 수 있습니다.",
666
+
667
+ "users.lastAdmin": "활성 관리자 계정이 최소 한 개는 있어야 합니다.",
668
+ "users.roleFor": "{email}의 역할",
669
+ "users.saveRoleFor": "{email}의 역할 저장",
670
+ "users.saveRole": "저장",
671
+ "users.invalidRole": "사용자 또는 관리자를 선택하세요.",
672
+
673
+ "languageAdmin.template": "번역 템플릿 다운로드",
674
+ "languageAdmin.templateHelp": "언어 코드와 해당 언어로 쓴 이름을 변경하고 문구를 번역한 뒤, 아래에 JSON을 붙여 넣으세요.",
675
+ "languageAdmin.tooLarge": "번역 팩은 512 KiB를 넘을 수 없습니다.",
676
+ "languageAdmin.invalidJson": "올바른 JSON 번역 팩을 입력하세요.",
677
+ "retention.artifactsSection": "아티팩트 보관 기간",
678
+ "retention.typeRulesSection": "아티팩트 유형별 규칙",
679
+ "retention.historySection": "감사 로그와 이벤트 기록",
680
+
681
+ "language.label": "언어",
682
+ "language.apply": "적용",
683
+ "languageAdmin.title": "언어 관리",
684
+ "languageAdmin.settings": "언어 설정",
685
+ "languageAdmin.enabled": "활성 언어",
686
+ "languageAdmin.default": "기본 언어",
687
+ "languageAdmin.defaultHelp": "기본 언어는 활성 언어 중에서 선택하세요. 방문자는 활성화된 다른 언어를 선택할 수 있습니다.",
688
+ "languageAdmin.save": "언어 설정 저장",
689
+ "languageAdmin.import": "번역 팩 가져오기",
690
+ "languageAdmin.pack": "번역 팩 JSON",
691
+ "languageAdmin.importHelp": "locale, nativeName, messages를 포함한 JSON 객체를 붙여 넣으세요. 최대 크기는 512 KiB입니다. 일부 문구만 번역해도 되며 누락된 문구는 영어로 표시됩니다. {count} 같은 치환 변수는 그대로 유지하세요.",
692
+ "languageAdmin.disabledHelp": "새로 가져온 언어는 비활성 상태로 등록됩니다. 준비가 되면 위에서 활성화하세요.",
693
+
694
+ "media.unavailable": "올바른 base64 미디어 콘텐츠가 없어 미리보기를 표시할 수 없습니다.",
695
+ "media.image": "이미지 아티팩트",
696
+ "media.encoding": "인코딩: {encoding}",
697
+ "media.source": "원본: {encoding}",
698
+ "inbox.type.shared-with-me": "나에게 공유됨",
699
+ "inbox.type.comment": "새 댓글",
700
+ "inbox.type.new-version": "새 버전",
701
+ "inbox.type.review-requested": "검토 요청",
702
+
703
+ "auth.invalidCredentials": "이메일 또는 비밀번호가 올바르지 않습니다.",
704
+ "password.mismatch": "새 비밀번호와 확인 비밀번호가 일치하지 않습니다.",
705
+ "password.changed": "비밀번호가 변경되었습니다.",
706
+ "viewer.filter": "필터",
707
+ "viewer.filterColumn": "{column}열 필터",
708
+ "viewer.ruleFilter": "규칙 ID로 필터",
709
+ "viewer.rowsShown": "전체 {total}행 중 {visible}행 표시",
710
+ "viewer.resultsShown": "전체 {total}개 결과 중 {visible}개 표시",
711
+ "csv.empty": "CSV 아티팩트가 비어 있습니다.",
712
+ "csv.error": "CSV를 해석하지 못했습니다: {error}. 원본을 텍스트로 표시합니다.",
713
+ "csv.column": "열 {column}",
714
+ "csv.rows": "데이터 {count}행",
715
+ "csv.columns": "{count}열",
716
+ "csv.firstRows": "처음 {count}행 표시",
717
+ "csv.firstColumns": "처음 {count}열 표시",
718
+ "sarif.invalid": "올바른 SARIF JSON이 아닙니다. 원본을 텍스트로 표시합니다.",
719
+ "sarif.shape": "SARIF 최상위 구조와 일치하지 않는 JSON입니다. 서식을 정리한 JSON을 표시합니다.",
720
+ "sarif.first": "전체 {total}개 결과 중 처음 {count}개를 표시합니다.",
721
+ "sarif.results": "결과 {count}개.",
722
+ "sarif.runs": "실행",
723
+ "sarif.resultsTitle": "결과",
724
+ "sarif.errors": "오류",
725
+ "sarif.warnings": "경고",
726
+ "sarif.notes": "참고",
727
+ "sarif.level": "수준",
728
+ "sarif.rule": "규칙",
729
+ "sarif.message": "메시지",
730
+ "sarif.location": "위치",
731
+ "sarif.tool": "도구",
732
+ "sarif.raw": "원본 SARIF JSON",
733
+ "react.disabled": "React 렌더링이 비활성화되어 있습니다. 샌드박스 프레임에서 실행하려면 ARTIFACTY_ENABLE_REACT_RENDERER=true로 설정하세요.",
734
+ "notebook.cells": "전체 {total}개 셀 중 처음 {count}개를 표시합니다.",
735
+ "notebook.outputs": "전체 {total}개 출력 중 처음 {count}개를 표시합니다.",
736
+ "notebook.unsupported": "지원하지 않는 출력 형식: {type}",
737
+ "notebook.image": "노트북 출력 이미지",
738
+ "notebook.truncated": "출력이 생략되었습니다. {size}바이트로 2 MB 제한을 초과합니다.",
739
+
740
+ "versions.manage": "버전 관리",
741
+ "ui.version": "버전",
742
+ "ui.created": "생성일",
743
+ "ui.bytes": "바이트",
744
+ "ui.action": "작업",
745
+ "ui.reason": "사유",
746
+ "ui.delete": "삭제",
747
+ "versions.view": "버전 보기",
748
+ "versions.repair": "버전 복구",
749
+ "ui.email": "이메일",
750
+ "ui.name": "이름",
751
+ "ui.password": "비밀번호",
752
+ "ui.revoke": "폐기",
753
+ "account.newToken": "새 API 토큰",
754
+ "account.profile": "프로필",
755
+ "account.createToken": "토큰 만들기",
756
+ "account.tokens": "API 토큰",
757
+ "ui.lastUsed": "마지막 사용",
758
+ "ui.status": "상태",
759
+ "account.noTokens": "API 토큰이 없습니다.",
760
+ "ui.title": "제목",
761
+ "ui.updated": "수정일",
762
+ "ui.none": "없습니다.",
763
+ "password.title": "비밀번호 변경",
764
+ "password.submit": "비밀번호 변경",
765
+ "password.current": "현재 비밀번호",
766
+ "password.new": "새 비밀번호",
767
+ "password.confirm": "비밀번호 확인",
768
+ "backup.status": "저장소 상태",
769
+ "backup.home": "저장소 경로",
770
+ "backup.bytes": "콘텐츠 크기(바이트)",
771
+ "backup.integrity": "무결성",
772
+ "backup.download": "백업 다운로드",
773
+ "backup.restore": "백업 복원",
774
+ "backup.json": "백업 JSON",
775
+ "users.importResults": "가져오기 결과",
776
+ "ui.role": "역할",
777
+ "users.resetRequired": "변경 필요",
778
+ "users.temporaryPassword": "임시 비밀번호",
779
+ "users.skipped": "건너뜀",
780
+ "users.failed": "실패",
781
+ "ui.row": "행",
782
+ "ui.error": "오류",
783
+ "users.create": "사용자 만들기",
784
+ "users.importTitle": "CSV에서 사용자 가져오기",
785
+ "users.import": "사용자 가져오기",
786
+ "users.existing": "기존 사용자",
787
+ "import.generic": "일반",
788
+ "login.setupHelp": "아직 사용자가 없습니다. 첫 번째 계정은 관리자가 됩니다.",
789
+ "account.copyToken": "지금 토큰을 복사하세요. Artifacty는 해시만 저장하므로 이 토큰을 다시 표시할 수 없습니다.",
790
+ "password.required": "계속하려면 비밀번호를 변경해야 합니다.",
791
+ "users.csvHelp": "열 이름은 email, name, role, password, password_reset_required를 사용하세요. 비밀번호를 비워 두면 자동으로 생성되며 첫 로그인 시 변경해야 합니다.",
792
+ "login.setup": "관리자 계정 만들기",
793
+ "login.create": "관리자 만들기",
794
+ "ui.never": "사용한 적 없음",
795
+ "ui.active": "활성",
796
+ "ui.disabled": "비활성",
797
+ "ui.disable": "비활성화",
798
+ "ui.enable": "활성화",
799
+ "ui.yes": "예",
800
+ "ui.no": "아니요",
801
+ "users.provided": "CSV에서 지정함",
802
+ "users.requirePassword": "비밀번호 변경 요구",
803
+ "account.tokenName": "토큰 이름",
804
+ "backup.ok": "정상",
805
+ "backup.attention": "확인 필요",
806
+ "versions.content": "v{version} 콘텐츠",
807
+ "users.summary": "생성 {created}명, 건너뜀 {skipped}명, 실패 {failed}명.",
808
+ "backup.complete": "복원이 완료되었습니다. 아티팩트 {count}개를 가져왔습니다",
809
+ "ui.role.user": "사용자",
810
+ "ui.role.admin": "관리자",
811
+ "backup.integrityHelp": "이전하기 전에 artifacty integrity 명령을 실행하세요. 현재 백업에는 참조된 버전 파일 중 읽을 수 있는 파일만 포함됩니다.",
812
+ "backup.exportHelp": "아티팩트 메타데이터와 버전 콘텐츠를 하나의 JSON 묶음으로 내보냅니다. 전체 범위에는 사용자, API 토큰, 감사 기록, 관계, 웹훅도 포함됩니다. 비밀 키는 포함되지 않으므로 복원 후 다시 발급해야 합니다.",
813
+ "backup.restoreHelp": "복원하면 서버의 아티팩트 기록과 버전 파일이 백업 내용으로 교체됩니다. 아티팩트 전용 백업은 사용자, 세션, API 토큰, 감사 로그를 변경하지 않습니다. 전체 백업은 사용자, API 토큰, 감사 기록, 관계, 웹훅도 교체합니다. 가져온 웹훅은 비활성화되므로 비밀 키를 다시 발급해야 합니다. 복원 동의 항목을 선택하세요. 기존 계정이 있는 서버에 전체 백업을 복원할 때만 사용자 교체 항목도 선택하세요.",
814
+
815
+ "update.releaseNotes": "변경 내용 보기",
816
+ "update.checkFailed": "최신 버전을 확인하지 못했습니다. 다시 확인해 주세요.",
817
+ "update.checkAgain": "업데이트를 진행하려면 다시 확인하여 새 동의 화면을 여세요.",
818
+ "update.available": "업데이트가 있습니다. v{version}로 업데이트할까요?",
819
+ "update.signIn": "세션이 만료되었거나 접근 권한이 없습니다. 다시 로그인하거나 화면을 새로고침하세요.",
820
+ "update.title": "서버 업데이트",
821
+ "update.installed": "설치된 버전",
822
+ "update.latest": "업데이트할 버전",
823
+ "update.check": "업데이트 확인",
824
+ "update.help": "이 화면에서는 Artifacty Linux systemd 사용자 서비스만 업데이트할 수 있습니다. 업데이트 확인을 누르면 설치 환경도 확인합니다.",
825
+ "update.backup": "선택한 버전을 설치하기 전에 백업을 만듭니다. 서버가 잠시 중지된 뒤 다시 시작됩니다. 이 화면을 열어 두면 다시 연결합니다.",
826
+ "update.manual": "다른 방식으로 설치했다면 해당 설치 방식의 업데이트 안내를 따라 주세요.",
827
+ "update.unsupported": "이 설치 환경에서는 브라우저에서 업데이트할 수 없습니다.",
828
+ "update.current": "최신 버전입니다.",
829
+ "update.confirm": "저장소를 백업하고 서버를 다시 시작하여 이 버전을 설치하는 데 동의합니다.",
830
+ "update.start": "백업 후 업데이트",
831
+ "update.progress": "업데이트 상태",
832
+ "update.refresh": "상태 새로고침",
833
+ "update.reconnect": "서버가 다시 시작 중이거나 연결할 수 없습니다. 다시 연결하고 있습니다…",
834
+ "update.queued": "시작 대기 중",
835
+ "update.running": "업데이트 진행 중",
836
+ "update.succeeded": "업데이트가 완료되었습니다. 새 버전을 사용하려면 화면을 새로고침하세요.",
837
+ "update.failed": "업데이트하지 못했습니다. 다시 시도하기 전에 오류를 확인하세요.",
838
+ "update.interrupted": "이전 업데이트가 중단되었습니다. 다시 시도하기 전에 서버 상태를 확인하세요.",
839
+ "update.expires": "확인 유효 기한",
840
+
841
+ "artifact.details": "세부 정보",
842
+ "nav.skip": "본문으로 이동",
843
+ "nav.artifacts": "아티팩트",
844
+ "nav.administration": "관리",
845
+ "nav.users": "사용자",
846
+ "nav.backup": "백업",
847
+ "nav.webhooks": "Webhook",
848
+ "nav.account": "계정",
849
+ "nav.accountTokens": "계정 및 API 토큰",
850
+ "nav.password": "비밀번호",
851
+ "nav.signIn": "로그인",
852
+ "nav.signOut": "로그아웃",
853
+ "nav.apiDocs": "API 문서",
854
+ "nav.versions": "버전 관리",
855
+
856
+ "dashboard.navigation": "대시보드 탐색",
857
+ "dashboard.more": "메뉴",
858
+ "dashboard.searchPlaceholder": "아티팩트 검색",
859
+ "dashboard.advanced": "상세 필터",
860
+ "dashboard.activeFilters": "{count}개 적용",
861
+ "filter.artifactType": "아티팩트 유형",
862
+ "filter.reviewStatus": "리뷰 상태",
863
+ "filter.mode": "검색 모드",
864
+ "filter.groupBy": "묶어서 보기",
865
+ "filter.relatedTo": "관련 아티팩트 ID",
866
+ "filter.relation": "관계",
867
+
281
868
  "sharing.staticPreview": "정적 미리보기: 스크립트와 외부 리소스는 비활성화됩니다.",
282
869
  "sharing.safeSource": "소스 미리보기: 공개 공유 링크에서는 실행 가능한 콘텐츠를 비활성화합니다.",
283
870
  "sharing.networkCondition": "모든 권한은 허용된 IP에서 접속해야 사용할 수 있습니다",
@@ -556,23 +1143,25 @@ const TRANSLATIONS = {
556
1143
 
557
1144
  export function resolveLocale(value) {
558
1145
  const normalized = String(value || "").trim().toLowerCase();
559
- if (SUPPORTED_LOCALES.includes(normalized)) {
1146
+ if (enabledLocales().includes(normalized)) {
560
1147
  return normalized;
561
1148
  }
562
- return DEFAULT_LOCALE;
1149
+ return defaultLocale();
563
1150
  }
564
1151
 
565
1152
  export function localeFromUrl(url) {
566
- return resolveLocale(url.searchParams.get("lang") || process.env.ARTIFACTY_LOCALE);
1153
+ return resolveLocale(url.searchParams.get("lang") || (languageContext.getStore() ? defaultLocale() : process.env.ARTIFACTY_LOCALE));
567
1154
  }
568
1155
 
569
1156
  export function localeFromBodyOrUrl(body = {}, url) {
570
- return resolveLocale(body.lang || url.searchParams.get("lang") || process.env.ARTIFACTY_LOCALE);
1157
+ return resolveLocale(body.lang || url.searchParams.get("lang") || (languageContext.getStore() ? defaultLocale() : process.env.ARTIFACTY_LOCALE));
571
1158
  }
572
1159
 
573
- export function createI18n(locale = DEFAULT_LOCALE) {
1160
+ export function createI18n(locale = defaultLocale()) {
574
1161
  const resolved = resolveLocale(locale);
575
- const messages = TRANSLATIONS[resolved] || TRANSLATIONS[DEFAULT_LOCALE];
1162
+ const messages = TRANSLATIONS[resolved]
1163
+ || languageContext.getStore()?.packs.find((pack) => pack.locale === resolved)?.messages
1164
+ || TRANSLATIONS[DEFAULT_LOCALE];
576
1165
 
577
1166
  return {
578
1167
  locale: resolved,
@@ -585,7 +1174,7 @@ export function createI18n(locale = DEFAULT_LOCALE) {
585
1174
 
586
1175
  export function localizedHref(href, locale) {
587
1176
  const resolved = resolveLocale(locale);
588
- if (resolved === DEFAULT_LOCALE) {
1177
+ if (resolved === DEFAULT_LOCALE && defaultLocale() === DEFAULT_LOCALE) {
589
1178
  return stripLocale(href);
590
1179
  }
591
1180
  const url = new URL(href, "http://artifacty.local");
@@ -594,7 +1183,10 @@ export function localizedHref(href, locale) {
594
1183
  }
595
1184
 
596
1185
  export function switchLocaleHref(currentPath, locale) {
597
- return localizedHref(currentPath || "/", locale);
1186
+ const url = new URL(currentPath || "/", "http://artifacty.local");
1187
+ // An explicit English choice must override a saved preference or server default.
1188
+ url.searchParams.set("lang", resolveLocale(locale));
1189
+ return `${url.pathname}${url.search}${url.hash}`;
598
1190
  }
599
1191
 
600
1192
  export function editorMessages(locale = DEFAULT_LOCALE) {