@podosoft/podokit 0.11.1 → 0.12.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.
Files changed (94) hide show
  1. package/README.md +7 -3
  2. package/dist/add.d.ts +16 -0
  3. package/dist/add.js +63 -20
  4. package/dist/assemble.js +9 -9
  5. package/dist/dev.d.ts +26 -0
  6. package/dist/dev.js +297 -0
  7. package/dist/index.d.ts +4 -0
  8. package/dist/index.js +82 -2
  9. package/dist/lib.d.ts +2 -0
  10. package/dist/lib.js +7 -1
  11. package/dist/locale.d.ts +22 -0
  12. package/dist/locale.js +184 -0
  13. package/dist/lockfile.d.ts +5 -3
  14. package/dist/lockfile.js +21 -5
  15. package/dist/remove.js +26 -23
  16. package/dist/templates/fullstack-nest-svelte/README.md +42 -0
  17. package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +1 -0
  18. package/dist/templates/fullstack-nest-svelte/apps/api/scripts/.keep +1 -0
  19. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/README.md +7 -5
  20. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/catalogs/app/en.json +1 -0
  21. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/catalogs/app/ko.json +1 -0
  22. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/locales/en.json +6 -0
  23. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/locales/ko.json +6 -0
  24. package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +3 -9
  25. package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +8 -11
  26. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/SKILL.md +63 -0
  27. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/agents/openai.yaml +4 -0
  28. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/references/catalog-format.md +49 -0
  29. package/dist/templates/fullstack-nest-svelte/dot-env.docker +2 -2
  30. package/dist/templates/fullstack-nest-svelte/dot-gitignore +3 -0
  31. package/dist/templates/fullstack-nest-svelte/dot-podokit/dev.json +4 -0
  32. package/dist/templates/fullstack-nest-svelte/tests/README.md +10 -4
  33. package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +187 -0
  34. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.controller.ts +9 -0
  35. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +5 -1
  36. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/auth-errors.ts +3 -0
  37. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/auth-redirect.ts +27 -0
  38. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +8 -3
  39. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
  40. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +564 -0
  41. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +564 -0
  42. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +13 -12
  43. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +90 -722
  44. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site.svelte.ts +8 -1
  45. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +4 -4
  46. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.svelte +2 -2
  47. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +4 -4
  48. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +8 -2
  49. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/general-settings.svelte +15 -4
  50. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +4 -4
  51. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +32 -35
  52. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +10 -3
  53. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +11 -7
  54. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +78 -0
  55. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +28 -1
  56. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -1
  57. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +15 -0
  58. package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +59 -6
  59. package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +1 -1
  60. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +1 -1
  61. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +5 -0
  62. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +2 -2
  63. package/dist/templates/modules/admin-dashboard/module.manifest.json +14 -3
  64. package/dist/templates/modules/auth/files/apps/api/scripts/configure-auth.mjs +22 -11
  65. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +6 -2
  66. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +50 -25
  67. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/SKILL.md +15 -2
  68. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/agents/openai.yaml +2 -2
  69. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +51 -0
  70. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/google.md +44 -0
  71. package/dist/templates/modules/auth/module.manifest.json +3 -0
  72. package/dist/templates/todo/README.md +42 -0
  73. package/dist/templates/todo/apps/api/Dockerfile +1 -0
  74. package/dist/templates/todo/apps/api/scripts/.keep +1 -0
  75. package/dist/templates/todo/apps/web/src/lib/i18n/README.md +7 -5
  76. package/dist/templates/todo/apps/web/src/lib/i18n/catalogs/app/en.json +1 -0
  77. package/dist/templates/todo/apps/web/src/lib/i18n/catalogs/app/ko.json +1 -0
  78. package/dist/templates/todo/apps/web/src/lib/i18n/locales/en.json +6 -0
  79. package/dist/templates/todo/apps/web/src/lib/i18n/locales/ko.json +6 -0
  80. package/dist/templates/todo/apps/web/vite.config.ts +3 -9
  81. package/dist/templates/todo/compose.dev.yaml +8 -11
  82. package/dist/templates/todo/dot-claude/skills/podokit-add-locale/SKILL.md +63 -0
  83. package/dist/templates/todo/dot-claude/skills/podokit-add-locale/agents/openai.yaml +4 -0
  84. package/dist/templates/todo/dot-claude/skills/podokit-add-locale/references/catalog-format.md +49 -0
  85. package/dist/templates/todo/dot-env.docker +2 -2
  86. package/dist/templates/todo/dot-gitignore +3 -0
  87. package/dist/templates/todo/dot-podokit/dev.json +4 -0
  88. package/dist/templates/todo/tests/README.md +10 -4
  89. package/dist/update.js +48 -7
  90. package/package.json +1 -1
  91. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/en.ts +0 -10
  92. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/ko.ts +0 -11
  93. package/dist/templates/todo/apps/web/src/lib/i18n/en.ts +0 -10
  94. package/dist/templates/todo/apps/web/src/lib/i18n/ko.ts +0 -11
@@ -1,731 +1,99 @@
1
- // Static message catalogs for the admin dashboard. Default locale is English.
2
- export const LOCALES = ["en", "ko"] as const;
3
- export type Locale = (typeof LOCALES)[number];
4
- export const defaultLocale: Locale = "en";
1
+ import type adminEnglish from "./catalogs/admin-dashboard/en.json";
2
+ import type appEnglish from "./catalogs/app/en.json";
5
3
 
6
- export const localeNames: Record<Locale, string> = {
7
- en: "English",
8
- ko: "한국어",
9
- };
4
+ export interface LocaleDefinition {
5
+ code: string;
6
+ name: string;
7
+ direction: "ltr" | "rtl";
8
+ enabled: boolean;
9
+ }
10
10
 
11
- const en = {
12
- common: { appName: "Admin" },
13
- landing: { title: "Welcome", subtitle: "This is your app's home page. Replace it with your own content — the admin area lives under /admin.", openAdmin: "Open admin" },
14
- footer: { support: "Support", terms: "Terms", privacy: "Privacy" },
15
- maintenance: { title: "Under maintenance", subtitle: "The site is temporarily unavailable. Please check back soon.", adminHint: "Administrators can sign in to manage the site.", signIn: "Admin sign in" },
16
- language: { label: "Language" },
17
- toolbar: { filter: "Filter", search: "Search", searchButton: "Search", all: "All" },
18
- theme: { toggle: "Toggle theme", light: "Light", dark: "Dark", system: "System" },
19
- setup2fa: {
20
- title: "Set up two-factor authentication",
21
- subtitle: "This site requires two-factor authentication. Add an authenticator app to continue.",
22
- passwordLabel: "Confirm your password",
23
- begin: "Continue",
24
- step1: "1. Scan with your authenticator app",
25
- step2: "2. Save your backup codes",
26
- step3: "3. Enter the 6-digit code",
27
- savedConfirm: "I've saved my backup codes",
28
- activate: "Activate and continue",
29
- signOut: "Sign out",
30
- wrongPassword: "That password is incorrect.",
31
- enableFailed: "Couldn't start setup. Please try again.",
32
- },
33
- auth: {
34
- signInTitle: "Sign in",
35
- signInDesc: "Enter your credentials to continue.",
36
- email: "Email",
37
- emailOrUsername: "Email or username",
38
- password: "Password",
39
- forgot: "Forgot?",
40
- signIn: "Sign in",
41
- signingIn: "Signing in…",
42
- magicLinkButton: "Email me a sign-in link",
43
- passkeyButton: "Use a passkey",
44
- magicLinkSent: "Check your email for a sign-in link.",
45
- magicLinkNeedsEmail: "Enter your email first.",
46
- otpButton: "Email me a code", otpCode: "One-time code", otpVerify: "Verify code",
47
- verify: "Verify",
48
- twoFactorPrompt: "Enter the 6-digit code from your authenticator app.",
49
- twoFactorBackupPrompt: "Enter one of your backup codes.",
50
- twoFactorCodeLabel: "Authentication code",
51
- backupCodeLabel: "Backup code",
52
- twoFactorUseBackup: "Use a backup code instead",
53
- twoFactorUseAuthenticator: "Use your authenticator app instead",
54
- twoFactorInvalidCode: "That code isn't valid. Check it and try again.",
55
- twoFactorLocked: "Too many attempts. Please wait a moment and try again.",
56
- noAccount: "No account?",
57
- signUp: "Sign up",
58
- signInFailed: "Sign in failed",
59
- createTitle: "Create account",
60
- createDesc: "Sign up to get started.",
61
- name: "Name",
62
- create: "Create account",
63
- creating: "Creating…",
64
- haveAccount: "Have an account?",
65
- signUpFailed: "Sign up failed",
66
- resetTitle: "Reset password",
67
- resetDesc: "We'll send a reset link to your email.",
68
- sendReset: "Send reset link",
69
- sending: "Sending…",
70
- resetSent: "If that email exists, a reset link is on its way.",
71
- backToSignIn: "Back to sign in",
72
- requestFailed: "Request failed",
73
- newTitle: "Set a new password",
74
- newPassword: "New password",
75
- updatePassword: "Update password",
76
- updating: "Updating…",
77
- missingToken: "Missing or invalid reset token.",
78
- resetFailed: "Reset failed",
79
- verifyTitle: "Verify your email",
80
- verifyDesc: "We sent a verification link to {email}. Open it to activate your account.",
81
- verifyDescNoEmail: "Enter your email to receive a new verification link.",
82
- verifySent: "Verification email sent — check your inbox.",
83
- verifyLinkError: "That verification link is invalid or has expired. Request a new one below.",
84
- resendVerification: "Resend verification email",
85
- emailNotVerified: "Verify your email before signing in.",
86
- approvalPendingTitle: "Waiting for approval",
87
- approvalPendingDesc: "Your account was created and is waiting for an administrator to approve it. Try signing in again after approval.",
88
- approvalPending: "Your account is waiting for administrator approval.",
89
- continueWith: "Continue with {provider}",
90
- },
91
- nav: { overview: "Overview", users: "Users", sessions: "Sessions", organizations: "Organizations", audit: "Audit log", account: "Account", settings: "Settings", home: "Back to home", signOut: "Sign out" },
92
- passkeys: {
93
- title: "Passkeys", subtitle: "Passwordless sign-in with your device (Face ID, fingerprint, security key).",
94
- name: "Name", namePlaceholder: "e.g. MacBook", add: "Add passkey", remove: "Remove", untitled: "Passkey",
95
- added: "Passkey added", addFailed: "Failed to add passkey", removed: "Passkey removed", removeFailed: "Failed to remove passkey",
96
- empty: "No passkeys yet.", created: "Created", total: "{count} passkeys",
97
- },
98
- acceptInvitation: {
99
- title: "Organization invitation", pending: "Accepting your invitation…",
100
- ok: "You have joined the organization.", error: "This invitation is invalid or has expired.",
101
- goToOrgs: "Go to organizations",
102
- },
103
- organizations: {
104
- title: "Organizations", subtitle: "Multi-tenant teams. Create organizations and invite members.",
105
- create: "New organization", createTitle: "Create organization", name: "Name", slug: "Slug",
106
- created: "Created", delete: "Delete", cancel: "Cancel", empty: "No organizations yet.", total: "{count} organizations",
107
- submit: "Create", createdMsg: "Organization created", deletedMsg: "Organization deleted", actionFailed: "Action failed",
108
- manage: "Manage", membersDesc: "Members and pending invitations.", members: "Members", pending: "Pending invitations",
109
- parent: "Parent organization", noParent: "None (top level)", managers: "Managers", managersHint: "Existing users to add as managers (any number).",
110
- inviteEmail: "Invite by email", invite: "Invite", invited: "Invitation sent", remove: "Remove", cancelInvite: "Cancel",
111
- },
112
- consent: {
113
- title: "Authorize access", subtitle: "An application is requesting access to your account.",
114
- requests: "It will be able to access:", allow: "Allow", deny: "Deny",
115
- },
116
- apiKeys: {
117
- title: "API keys", subtitle: "Personal keys for programmatic access. Send as an x-api-key header.",
118
- name: "Name", namePlaceholder: "e.g. CI pipeline", create: "Create key",
119
- keyColumn: "Key", created: "Created", untitled: "Untitled", revoke: "Revoke",
120
- revoked: "Key revoked", revokeFailed: "Failed to revoke key", empty: "No API keys yet.", total: "{count} API keys",
121
- createdTitle: "API key created", createdDesc: "Copy it now — you will not be able to see it again.",
122
- copy: "Copy", copied: "Copied to clipboard", done: "Done",
123
- },
124
- general: {
125
- tab: "General",
126
- brandingTitle: "Branding", brandingDesc: "The site name and icon shown in the browser and across the app.",
127
- siteName: "Site name", siteNamePlaceholder: "My App",
128
- siteNameHint: "Used as the browser tab title and wherever the app name appears.",
129
- favicon: "Browser icon (favicon)",
130
- faviconHint: "SVG (recommended) or a square PNG 512×512, ≤ 1 MB. Applied to every page.",
131
- iconSaved: "Icon updated.", iconError: "Failed to upload the icon.", iconTooBig: "Icon must be 1 MB or smaller.",
132
- brandColor: "Brand color",
133
- appearanceTitle: "Appearance", appearanceDesc: "Choose the base colors and corner style for the app. Changes apply after you save.",
134
- themePreset: "Base theme", featuredThemes: "Featured themes", moreThemes: "Show more themes", hideThemes: "Show fewer themes",
135
- quickSettings: "Quick settings",
136
- accentColor: "Brand color", accentColorHint: "Replaces the theme's primary color. Leave it empty to use the theme color.", clearColor: "Clear brand color",
137
- radius: "Corner style", radiusDefault: "App default", radiusNone: "Square", radiusSmall: "Small", radiusMedium: "Medium", radiusLarge: "Large",
138
- advanced: "Fine-tune colors", advancedHide: "Close color settings",
139
- resetTheme: "Clear color adjustments", saveTheme: "Save changes", restoreDefaults: "Restore defaults",
140
- preview: "Preview", previewLight: "Light", previewDark: "Dark",
141
- previewApp: "Workspace", previewNav: "Overview", previewSettings: "Settings",
142
- previewHeading: "Theme preview", previewBody: "Colors and corner styles are shown here before you save.",
143
- previewPrimary: "Save", previewOutline: "Cancel", previewBadge: "Active",
144
- themeTokens: {
145
- background: "Background", card: "Card", foreground: "Foreground",
146
- mutedForeground: "Muted text", border: "Border", secondary: "Secondary",
147
- accent: "Accent", primary: "Primary", primaryForeground: "Primary text",
148
- },
149
- infoTitle: "Site info", infoDesc: "Optional details used in meta tags, the footer, and links.",
150
- description: "Description", descriptionPlaceholder: "A short tagline for your site",
151
- supportEmail: "Support email", footerText: "Footer text",
152
- termsUrl: "Terms URL", privacyUrl: "Privacy URL",
153
- locale: "Default language", timezone: "Default timezone",
154
- opsTitle: "Operations",
155
- maintenance: "Maintenance mode", maintenanceDesc: "Show a maintenance page to non-admins.",
156
- allowSignup: "Allow public sign-up", allowSignupDesc: "Turn off to make the app invite-only.",
157
- save: "Save changes", saving: "Saving…", saved: "General settings saved.", saveError: "Failed to save settings.",
158
- },
159
- settings: {
160
- tab: "Authentication",
161
- title: "Settings", subtitle: "Which authentication features are enabled, and how they are configured.",
162
- authFeaturesTitle: "Authentication features", authFeaturesSubtitle: "Toggle sign-in methods and security features. Changes apply immediately.",
163
- serverConfigTitle: "Server configuration", serverConfigSubtitle: "Set through environment variables and applied at startup (read-only).",
164
- enabled: "Enabled", disabled: "Disabled", configuredVia: "Configured via {env}", manage: "Manage",
165
- saved: "Setting updated", saveFailed: "Failed to update setting",
166
- emailPassword: "Email & password", emailPasswordDesc: "Sign in with an email address and password.",
167
- emailVerification: "Email verification", emailVerificationDesc: "Require new sign-ups to confirm their address.",
168
- signupApproval: "Sign-up approval", signupApprovalDesc: "Require an administrator to approve new self-registered users. Disabling this only affects future registrations.",
169
- magicLink: "Magic link", magicLinkDesc: "Passwordless sign-in via an emailed link.",
170
- emailOtp: "Email one-time code", emailOtpDesc: "Passwordless sign-in with an emailed code.",
171
- username: "Username sign-in", usernameDesc: "Let users sign in with a username instead of an email.",
172
- multiSession: "Multi-account sessions", multiSessionDesc: "Let one browser stay signed into several accounts and switch between them.",
173
- phoneNumber: "Phone number", phoneNumberDesc: "Register and verify a phone number via SMS (configure a provider for real delivery).",
174
- apiKey: "API keys", apiKeyDesc: "Let users create personal API keys for programmatic access.",
175
- passkey: "Passkeys", passkeyDesc: "Let users register passkeys for passwordless sign-in.",
176
- organization: "Organizations", organizationDesc: "Multi-tenant organizations with members and email invitations.",
177
- oidcProvider: "OIDC provider", oidcProviderDesc: "Act as an identity provider so other apps can sign in with this one.",
178
- twoFactor: "Two-factor authentication", twoFactorDesc: "TOTP second factor with backup codes.",
179
- require2fa: "Require two-factor", require2faDesc: "Every user must enrol in 2FA after signing up before using the app.",
180
- socialProviders: "Social providers", socialProvidersDesc: "OAuth sign-in (Google, GitHub).",
181
- breachCheck: "Breached-password check", breachCheckDesc: "Reject passwords found in known breaches (Have I Been Pwned).",
182
- accountDeletion: "Account deletion", accountDeletionDesc: "Let users delete their own account.",
183
- auditLog: "Audit log", auditLogDesc: "Record security-relevant actions.",
184
- signInTitle: "Sign-in methods", signInSubtitle: "How users authenticate. Toggle methods on/off; social providers open a popup for credentials.",
185
- emailTitle: "Email", emailSubtitle: "Outgoing mail (SMTP) and the email-verification policy.",
186
- securityTitle: "Security & policy", securitySubtitle: "Second factor, breached-password rejection, account deletion, and audit logging.",
187
- advancedTitle: "Advanced", advancedSubtitle: "API keys, organizations, and acting as an OIDC identity provider.",
188
- socialTitle: "Social login", socialSubtitle: "Paste OAuth credentials to enable sign-in with a provider. Applied immediately — no restart. Secrets are stored encrypted.",
189
- smtpTitle: "SMTP (email delivery)", smtpSubtitle: "Outgoing mail for sign-in links, verification, and invitations. Applied immediately.",
190
- google: "Google", github: "GitHub",
191
- clientId: "Client ID", clientSecret: "Client secret", redirectUri: "Redirect URI", secretSet: "•••••••• (saved — leave blank to keep)",
192
- save: "Save", cancel: "Cancel", configure: "Configure", notConfigured: "Not configured", configuredOff: "Configured (off)",
193
- socialDialogDesc: "Paste the OAuth client ID and secret from the provider's console, then enable it. Applied immediately.",
194
- smtpEnabled: "Enabled", smtpHost: "Host", smtpPort: "Port", smtpUser: "Username", smtpPass: "Password", smtpFrom: "From address", smtpSecure: "TLS (secure)",
195
- addProvider: "Add provider", provider: "Provider", remove: "Remove", smtpConfigure: "SMTP settings",
196
- edit: "Edit", close: "Close", back: "Back", noProviders: "No providers added yet.",
197
- socialCardDesc: "Sign in with Google, GitHub, Apple and other OAuth providers.",
198
- },
199
- dashboard: { title: "Dashboard", welcome: "Welcome back, {name}.", yourRole: "Your role", signedInAs: "Signed in as", unverifiedUsers: "Unverified users" },
200
- audit: { title: "Audit log", when: "When", actor: "Actor", action: "Action", target: "Target", ip: "IP", empty: "No audit entries.", loadFailed: "Failed to load audit log", total: "{count} entries", searchField: "Actor or target" },
201
- users: {
202
- title: "Users",
203
- search: "Search by email…",
204
- name: "Name",
205
- email: "Email",
206
- role: "Role",
207
- status: "Status",
208
- active: "Active",
209
- banned: "Banned",
210
- makeUser: "Make user",
211
- makeAdmin: "Make admin",
212
- ban: "Ban",
213
- unban: "Unban",
214
- empty: "No users.",
215
- total: "{count} users",
216
- previous: "Previous",
217
- next: "Next",
218
- roleSet: "Role set to {role}",
219
- userBanned: "User banned",
220
- userUnbanned: "User unbanned",
221
- loadFailed: "Failed to load users",
222
- actionFailed: "Action failed",
223
- addUser: "Add user",
224
- createTitle: "Create user",
225
- password: "Password",
226
- create: "Create",
227
- cancel: "Cancel",
228
- close: "Close",
229
- roleAdmin: "Admin",
230
- roleUser: "User",
231
- roleModerator: "Moderator",
232
- userCreated: "User created",
233
- setPassword: "Set password",
234
- newPassword: "New password",
235
- save: "Save",
236
- passwordSet: "Password updated",
237
- delete: "Delete",
238
- deleteTitle: "Delete user",
239
- deleteConfirm: "Delete {email}? This cannot be undone.",
240
- userDeleted: "User deleted",
241
- confirmPassword: "Confirm password",
242
- confirmNewPassword: "Confirm new password",
243
- passwordMismatch: "Passwords do not match",
244
- edit: "Edit",
245
- editTitle: "Edit user",
246
- userUpdated: "User updated",
247
- impersonate: "Impersonate",
248
- banTitle: "Ban user",
249
- banReason: "Reason (optional)",
250
- banExpiryDays: "Expires in (days, optional)",
251
- banExpiry: "Ban expires",
252
- banPermanent: "Permanent ban",
253
- joined: "Joined",
254
- stopImpersonating: "Stop impersonating",
255
- impersonatingAs: "Impersonating {email}",
256
- manage: "Manage",
257
- revokeSession: "Revoke",
258
- verified: "Verified",
259
- unverified: "Unverified",
260
- sendVerification: "Send verification email",
261
- verificationSent: "Verification email sent",
262
- markVerified: "Mark verified",
263
- markedVerified: "Marked as verified",
264
- sendResetEmail: "Send password reset email",
265
- resetEmailSent: "Password reset email sent",
266
- sendVerificationOnCreate: "Send verification email",
267
- pendingApproval: "Pending approval",
268
- approve: "Approve sign-up",
269
- approved: "Sign-up approved",
270
- },
271
- sessions: {
272
- title: "Active sessions",
273
- device: "Device",
274
- ip: "IP",
275
- since: "Since",
276
- revoke: "Revoke",
277
- unknown: "Unknown",
278
- empty: "No active sessions.",
279
- revoked: "Session revoked",
280
- loadFailed: "Failed to load sessions",
281
- revokeFailed: "Failed to revoke",
282
- status: "Status",
283
- current: "Current",
284
- signOutOthers: "Sign out other sessions",
285
- accountsTitle: "Signed-in accounts", switchAccount: "Switch", switchFailed: "Failed to switch account",
286
- othersRevoked: "Signed out other sessions",
287
- total: "{count} sessions",
288
- },
289
- adminSessions: {
290
- title: "User sessions",
291
- user: "User",
292
- device: "Device",
293
- ip: "IP",
294
- since: "Since",
295
- expires: "Expires",
296
- impersonated: "Impersonated",
297
- revoke: "Revoke session",
298
- revokeAll: "Revoke all for user",
299
- unknown: "Unknown",
300
- empty: "No active sessions.",
301
- revoked: "Session revoked",
302
- allRevoked: "All sessions revoked for user",
303
- loadFailed: "Failed to load sessions",
304
- total: "{count} sessions",
305
- },
306
- account: {
307
- title: "Account",
308
- profile: "Profile",
309
- name: "Name",
310
- username: "Username",
311
- phone: "Phone number", phoneCode: "Verification code", phoneSendCode: "Send code", phoneVerify: "Verify", phoneCodeSent: "Code sent", phoneVerified: "Phone verified",
312
- email: "Email",
313
- role: "Role",
314
- changePassword: "Change password",
315
- currentPassword: "Current password",
316
- newPassword: "New password",
317
- update: "Update password",
318
- updating: "Updating…",
319
- changed: "Password changed",
320
- changeFailed: "Failed to change password",
321
- nav: { profile: "Profile", security: "Security", connected: "Connected", sessions: "Sessions", apiKeys: "API keys", danger: "Danger zone" },
322
- twoFactorTitle: "Two-factor authentication",
323
- twoFactorDesc: "Add a one-time code from your authenticator app at sign-in.",
324
- twoFactorOn: "Enabled",
325
- twoFactorOff: "Disabled",
326
- enable: "Enable",
327
- disable: "Disable",
328
- password: "Password",
329
- scanHint: "Add this to your authenticator app, then enter the 6-digit code.",
330
- setupKey: "Setup key (otpauth URI)",
331
- backupCodes: "Backup codes — store them somewhere safe",
332
- backupCodesHint: "Each code works once if you lose your authenticator. Save them now — they won't be shown again.",
333
- downloadBackupCodes: "Download",
334
- regenerateBackupCodes: "Regenerate backup codes",
335
- regenerateBackupCodesHint: "Generating a new set invalidates your previous backup codes.",
336
- backupCodesRegenerated: "New backup codes generated",
337
- code: "Verification code",
338
- verify: "Verify",
339
- twoFactorEnabled: "Two-factor enabled",
340
- twoFactorDisabled: "Two-factor disabled",
341
- connectedTitle: "Connected accounts",
342
- connectedDesc: "Link social accounts to sign in with them.",
343
- connect: "Connect",
344
- disconnect: "Disconnect",
345
- connected: "Connected",
346
- notConnected: "Not connected",
347
- disconnected: "Disconnected",
348
- dangerTitle: "Delete account",
349
- dangerDesc: "Permanently delete your account and all associated data.",
350
- deleteAccount: "Delete account",
351
- deleteAccountConfirm: "This permanently deletes your account and cannot be undone. Enter your password to confirm.",
352
- accountDeleted: "Account deleted",
353
- save: "Save changes",
354
- saved: "Profile updated",
355
- saveFailed: "Failed to save profile",
356
- saving: "Saving…",
357
- changeEmail: "Change email",
358
- emailChanged: "Email address updated",
359
- emailChangePending: "Check your current inbox to approve the change",
360
- verified: "Verified",
361
- unverified: "Unverified",
362
- },
363
- // Modules add their i18n blocks here.
364
- // podokit:begin:messages-en
365
- // podokit:end:messages-en
11
+ type JsonObject = Record<string, unknown>;
12
+
13
+ export type ModuleMessages = {
14
+ // Modules add their typed message namespaces here.
15
+ // podokit:begin:message-types
16
+ // podokit:end:message-types
366
17
  };
367
18
 
368
- export type Messages = typeof en;
19
+ export type Messages = typeof adminEnglish & typeof appEnglish & ModuleMessages;
20
+ export type Locale = string;
369
21
 
370
- const ko: Messages = {
371
- common: { appName: "관리자" },
372
- landing: { title: "환영합니다", subtitle: "앱의 홈 페이지입니다. 원하는 내용으로 교체하세요 — 관리자 기능은 /admin 에 있습니다.", openAdmin: "관리자 열기" },
373
- footer: { support: "고객지원", terms: "이용약관", privacy: "개인정보처리방침" },
374
- maintenance: { title: "점검 중", subtitle: "사이트를 일시적으로 사용할 수 없습니다. 잠시 후 다시 확인해 주세요.", adminHint: "관리자는 로그인하여 사이트를 관리할 수 있습니다.", signIn: "관리자 로그인" },
375
- language: { label: "언어" },
376
- toolbar: { filter: "필터", search: "검색", searchButton: "검색", all: "전체" },
377
- theme: { toggle: "테마 전환", light: "라이트", dark: "다크", system: "시스템" },
378
- setup2fa: {
379
- title: "2단계 인증 설정",
380
- subtitle: "이 사이트는 2단계 인증이 필수입니다. 계속하려면 인증 앱을 등록하세요.",
381
- passwordLabel: "비밀번호 확인",
382
- begin: "계속",
383
- step1: "1. 인증 앱으로 스캔",
384
- step2: "2. 백업 코드 저장",
385
- step3: "3. 6자리 코드 입력",
386
- savedConfirm: "백업 코드를 저장했습니다",
387
- activate: "활성화하고 계속",
388
- signOut: "로그아웃",
389
- wrongPassword: "비밀번호가 올바르지 않습니다.",
390
- enableFailed: "설정을 시작할 수 없습니다. 다시 시도해 주세요.",
391
- },
392
- auth: {
393
- signInTitle: "로그인",
394
- signInDesc: "계속하려면 로그인하세요.",
395
- email: "이메일",
396
- emailOrUsername: "이메일 또는 사용자명",
397
- password: "비밀번호",
398
- forgot: "비밀번호 찾기",
399
- signIn: "로그인",
400
- signingIn: "로그인 중…",
401
- magicLinkButton: "로그인 링크 이메일로 받기",
402
- passkeyButton: "패스키 사용",
403
- magicLinkSent: "이메일의 로그인 링크를 확인하세요.",
404
- magicLinkNeedsEmail: "먼저 이메일을 입력하세요.",
405
- otpButton: "코드 이메일로 받기", otpCode: "일회용 코드", otpVerify: "코드 확인",
406
- verify: "확인",
407
- twoFactorPrompt: "인증 앱의 6자리 코드를 입력하세요.",
408
- twoFactorBackupPrompt: "백업 코드 중 하나를 입력하세요.",
409
- twoFactorCodeLabel: "인증 코드",
410
- backupCodeLabel: "백업 코드",
411
- twoFactorUseBackup: "백업 코드로 대신 인증",
412
- twoFactorUseAuthenticator: "인증 앱으로 대신 인증",
413
- twoFactorInvalidCode: "코드가 올바르지 않습니다. 다시 확인해 주세요.",
414
- twoFactorLocked: "시도가 너무 많습니다. 잠시 후 다시 시도해 주세요.",
415
- noAccount: "계정이 없으신가요?",
416
- signUp: "회원가입",
417
- signInFailed: "로그인에 실패했습니다",
418
- createTitle: "회원가입",
419
- createDesc: "가입하고 시작하세요.",
420
- name: "이름",
421
- create: "계정 만들기",
422
- creating: "생성 중…",
423
- haveAccount: "이미 계정이 있으신가요?",
424
- signUpFailed: "회원가입에 실패했습니다",
425
- resetTitle: "비밀번호 재설정",
426
- resetDesc: "이메일로 재설정 링크를 보내드립니다.",
427
- sendReset: "재설정 링크 보내기",
428
- sending: "전송 중…",
429
- resetSent: "해당 이메일이 존재하면 재설정 링크가 전송됩니다.",
430
- backToSignIn: "로그인으로 돌아가기",
431
- requestFailed: "요청에 실패했습니다",
432
- newTitle: "새 비밀번호 설정",
433
- newPassword: "새 비밀번호",
434
- updatePassword: "비밀번호 변경",
435
- updating: "변경 중…",
436
- missingToken: "재설정 토큰이 없거나 유효하지 않습니다.",
437
- verifyTitle: "이메일 인증",
438
- verifyDesc: "{email} 주소로 인증 링크를 보냈습니다. 링크를 열어 계정을 활성화하세요.",
439
- verifyDescNoEmail: "새 인증 링크를 받을 이메일을 입력하세요.",
440
- verifySent: "인증 메일을 보냈습니다 — 받은편지함을 확인하세요.",
441
- verifyLinkError: "인증 링크가 유효하지 않거나 만료되었습니다. 아래에서 다시 요청하세요.",
442
- resendVerification: "인증 메일 다시 보내기",
443
- emailNotVerified: "로그인 전에 이메일을 인증해 주세요.",
444
- approvalPendingTitle: "승인 대기 중",
445
- approvalPendingDesc: "계정이 생성되었으며 관리자의 승인을 기다리고 있습니다. 승인 후 다시 로그인해 주세요.",
446
- approvalPending: "관리자의 계정 승인을 기다리고 있습니다.",
447
- continueWith: "{provider}(으)로 계속",
448
- resetFailed: "재설정에 실패했습니다",
449
- },
450
- nav: { overview: "개요", users: "회원", sessions: "세션", organizations: "조직", audit: "감사 로그", account: "계정", settings: "설정", home: "홈으로 돌아가기", signOut: "로그아웃" },
451
- passkeys: {
452
- title: "패스키", subtitle: "기기로 비밀번호 없이 로그인(Face ID, 지문, 보안 키).",
453
- name: "이름", namePlaceholder: "예: MacBook", add: "패스키 추가", remove: "삭제", untitled: "패스키",
454
- added: "패스키를 추가했습니다", addFailed: "패스키 추가에 실패했습니다", removed: "패스키를 삭제했습니다", removeFailed: "패스키 삭제에 실패했습니다",
455
- empty: "아직 패스키가 없습니다.", created: "생성일", total: "패스키 {count}개",
456
- },
457
- acceptInvitation: {
458
- title: "조직 초대", pending: "초대를 수락하는 중…",
459
- ok: "조직에 합류했습니다.", error: "유효하지 않거나 만료된 초대입니다.",
460
- goToOrgs: "조직으로 이동",
461
- },
462
- organizations: {
463
- title: "조직", subtitle: "멀티테넌트 팀. 조직을 만들고 멤버를 초대합니다.",
464
- create: "새 조직", createTitle: "조직 만들기", name: "이름", slug: "슬러그",
465
- created: "생성일", delete: "삭제", cancel: "취소", empty: "아직 조직이 없습니다.", total: "조직 {count}개",
466
- submit: "만들기", createdMsg: "조직을 만들었습니다", deletedMsg: "조직을 삭제했습니다", actionFailed: "작업에 실패했습니다",
467
- manage: "관리", membersDesc: "멤버와 대기 중인 초대.", members: "멤버", pending: "대기 중 초대",
468
- parent: "상위 조직", noParent: "없음(최상위)", managers: "매니저", managersHint: "매니저로 추가할 기존 사용자(여러 명 가능).",
469
- inviteEmail: "이메일로 초대", invite: "초대", invited: "초대를 보냈습니다", remove: "제거", cancelInvite: "취소",
470
- },
471
- consent: {
472
- title: "접근 승인", subtitle: "한 애플리케이션이 계정 접근을 요청합니다.",
473
- requests: "접근 가능 범위:", allow: "허용", deny: "거부",
474
- },
475
- apiKeys: {
476
- title: "API 키", subtitle: "프로그래밍 방식 접근용 개인 키. x-api-key 헤더로 전송합니다.",
477
- name: "이름", namePlaceholder: "예: CI 파이프라인", create: "키 생성",
478
- keyColumn: "키", created: "생성일", untitled: "제목 없음", revoke: "폐기",
479
- revoked: "키를 폐기했습니다", revokeFailed: "키 폐기에 실패했습니다", empty: "아직 API 키가 없습니다.", total: "API 키 {count}개",
480
- createdTitle: "API 키 생성됨", createdDesc: "지금 복사하세요 — 다시 볼 수 없습니다.",
481
- copy: "복사", copied: "클립보드에 복사됨", done: "완료",
482
- },
483
- general: {
484
- tab: "일반",
485
- brandingTitle: "브랜딩", brandingDesc: "브라우저와 앱 전반에 표시되는 사이트 이름과 아이콘입니다.",
486
- siteName: "사이트 이름", siteNamePlaceholder: "My App",
487
- siteNameHint: "브라우저 탭 제목과 앱 이름이 쓰이는 곳에 사용됩니다.",
488
- favicon: "브라우저 아이콘 (favicon)",
489
- faviconHint: "SVG(권장) 또는 정사각 PNG 512×512, 1 MB 이하. 모든 페이지에 적용됩니다.",
490
- iconSaved: "아이콘을 변경했습니다.", iconError: "아이콘 업로드에 실패했습니다.", iconTooBig: "아이콘은 1 MB 이하여야 합니다.",
491
- brandColor: "브랜드 색상",
492
- appearanceTitle: "화면 테마", appearanceDesc: "앱의 기본 색상과 모서리 형태를 설정합니다. 저장하면 모든 화면에 적용됩니다.",
493
- themePreset: "기본 테마", featuredThemes: "주요 테마", moreThemes: "테마 더 보기", hideThemes: "테마 접기",
494
- quickSettings: "빠른 설정",
495
- accentColor: "브랜드 색상", accentColorHint: "선택한 테마의 주요 색상을 바꿉니다. 비우면 테마 색상을 사용합니다.", clearColor: "브랜드 색상 지우기",
496
- radius: "모서리 형태", radiusDefault: "앱 기본값", radiusNone: "각지게", radiusSmall: "작게", radiusMedium: "보통", radiusLarge: "크게",
497
- advanced: "세부 색상 설정", advancedHide: "세부 설정 닫기",
498
- resetTheme: "세부 색상 지우기", saveTheme: "변경 저장", restoreDefaults: "기본값으로 복원",
499
- preview: "미리보기", previewLight: "라이트", previewDark: "다크",
500
- previewApp: "워크스페이스", previewNav: "대시보드", previewSettings: "설정",
501
- previewHeading: "테마 미리보기", previewBody: "저장하기 전에 색상과 모서리 형태를 확인할 수 있습니다.",
502
- previewPrimary: "저장", previewOutline: "취소", previewBadge: "사용 중",
503
- themeTokens: {
504
- background: "배경", card: "카드", foreground: "전경(글자)",
505
- mutedForeground: "흐린 글자", border: "테두리", secondary: "보조",
506
- accent: "강조", primary: "기본", primaryForeground: "기본 글자",
507
- },
508
- infoTitle: "사이트 정보", infoDesc: "메타 태그·푸터·링크에 쓰이는 선택 항목입니다.",
509
- description: "설명", descriptionPlaceholder: "사이트를 한 줄로 소개",
510
- supportEmail: "지원 이메일", footerText: "푸터 문구",
511
- termsUrl: "약관 URL", privacyUrl: "개인정보 URL",
512
- locale: "기본 언어", timezone: "기본 시간대",
513
- opsTitle: "운영",
514
- maintenance: "유지보수 모드", maintenanceDesc: "관리자 외 사용자에게 점검 페이지를 표시합니다.",
515
- allowSignup: "공개 가입 허용", allowSignupDesc: "끄면 초대 전용으로 전환됩니다.",
516
- save: "변경 저장", saving: "저장 중…", saved: "일반 설정을 저장했습니다.", saveError: "설정 저장에 실패했습니다.",
517
- },
518
- settings: {
519
- tab: "인증",
520
- title: "설정", subtitle: "활성화된 인증 기능과 구성 방법입니다.",
521
- authFeaturesTitle: "인증 기능", authFeaturesSubtitle: "로그인 수단과 보안 기능을 켜고 끕니다. 변경은 즉시 적용됩니다.",
522
- serverConfigTitle: "서버 구성", serverConfigSubtitle: "환경변수로 설정되어 시작 시 적용됩니다(읽기 전용).",
523
- enabled: "활성", disabled: "비활성", configuredVia: "{env} 로 구성", manage: "관리",
524
- saved: "설정을 변경했습니다", saveFailed: "설정 변경에 실패했습니다",
525
- emailPassword: "이메일·비밀번호", emailPasswordDesc: "이메일과 비밀번호로 로그인.",
526
- emailVerification: "이메일 인증", emailVerificationDesc: "신규 가입 시 이메일 확인 요구.",
527
- signupApproval: "회원가입 승인", signupApprovalDesc: "신규 자가 가입 사용자를 관리자가 승인해야 로그인할 수 있습니다. 끄면 이후 가입부터 자동 승인됩니다.",
528
- magicLink: "매직 링크", magicLinkDesc: "이메일 링크로 비밀번호 없이 로그인.",
529
- emailOtp: "이메일 일회용 코드", emailOtpDesc: "이메일로 받은 코드로 비밀번호 없이 로그인.",
530
- username: "사용자명 로그인", usernameDesc: "이메일 대신 사용자명으로 로그인.",
531
- multiSession: "다중 계정 세션", multiSessionDesc: "한 브라우저에서 여러 계정에 로그인하고 전환.",
532
- phoneNumber: "전화번호", phoneNumberDesc: "SMS로 전화번호 등록·인증(실발송은 프로바이더 연동 필요).",
533
- apiKey: "API 키", apiKeyDesc: "프로그래밍 방식 접근을 위한 개인 API 키 발급을 허용.",
534
- passkey: "패스키", passkeyDesc: "비밀번호 없는 로그인을 위한 패스키 등록을 허용.",
535
- organization: "조직", organizationDesc: "멤버와 이메일 초대를 갖춘 멀티테넌트 조직.",
536
- oidcProvider: "OIDC 제공자", oidcProviderDesc: "다른 앱이 이 앱으로 로그인하도록 신원 제공자 역할.",
537
- twoFactor: "2단계 인증", twoFactorDesc: "TOTP 2차 인증 + 백업 코드.",
538
- require2fa: "2FA 필수화", require2faDesc: "모든 사용자가 가입 후 2FA를 등록해야 앱을 사용할 수 있습니다.",
539
- socialProviders: "소셜 로그인", socialProvidersDesc: "OAuth 로그인 (Google, GitHub).",
540
- breachCheck: "유출 비밀번호 검사", breachCheckDesc: "유출된 비밀번호 거부 (Have I Been Pwned).",
541
- accountDeletion: "계정 삭제", accountDeletionDesc: "사용자가 자신의 계정을 삭제.",
542
- auditLog: "감사 로그", auditLogDesc: "보안 관련 동작 기록.",
543
- signInTitle: "로그인 수단", signInSubtitle: "사용자가 인증하는 방법. 각 수단을 켜고 끄며, 소셜 로그인은 팝업에서 자격증명을 입력합니다.",
544
- emailTitle: "이메일", emailSubtitle: "메일 발송(SMTP) 설정과 이메일 인증 정책.",
545
- securityTitle: "보안·정책", securitySubtitle: "2단계 인증, 유출 비밀번호 거부, 계정 삭제, 감사 로그.",
546
- advancedTitle: "고급", advancedSubtitle: "API 키, 조직, OIDC 공급자(자체 IdP) 역할.",
547
- socialTitle: "소셜 로그인", socialSubtitle: "OAuth 자격증명을 붙여넣으면 해당 provider 로그인이 켜집니다. 재시작 없이 즉시 적용되며, 비밀은 암호화 저장됩니다.",
548
- smtpTitle: "SMTP(이메일 발송)", smtpSubtitle: "로그인 링크·인증·초대 발송용. 즉시 적용됩니다.",
549
- google: "Google", github: "GitHub",
550
- clientId: "Client ID", clientSecret: "Client secret", redirectUri: "Redirect URI", secretSet: "•••••••• (저장됨 — 비우면 유지)",
551
- save: "저장", cancel: "취소", configure: "설정", notConfigured: "미설정", configuredOff: "설정됨(꺼짐)",
552
- socialDialogDesc: "provider 콘솔의 OAuth client ID와 secret을 붙여넣고 사용을 켜세요. 즉시 적용됩니다.",
553
- smtpEnabled: "사용", smtpHost: "호스트", smtpPort: "포트", smtpUser: "사용자", smtpPass: "비밀번호", smtpFrom: "발신 주소", smtpSecure: "TLS(보안)",
554
- addProvider: "공급자 추가", provider: "공급자", remove: "삭제", smtpConfigure: "SMTP 설정",
555
- edit: "편집", close: "닫기", back: "뒤로", noProviders: "아직 추가된 공급자가 없습니다.",
556
- socialCardDesc: "Google, GitHub, Apple 등 OAuth 공급자로 로그인.",
557
- },
558
- dashboard: { title: "대시보드", welcome: "{name}님, 환영합니다.", yourRole: "내 권한", signedInAs: "로그인 계정", unverifiedUsers: "미인증 사용자" },
559
- audit: { title: "감사 로그", when: "시각", actor: "수행자", action: "동작", target: "대상", ip: "IP", empty: "감사 기록이 없습니다.", loadFailed: "감사 로그를 불러오지 못했습니다", total: "항목 {count}개", searchField: "수행자·대상" },
560
- users: {
561
- title: "회원",
562
- search: "이메일로 검색…",
563
- name: "이름",
564
- email: "이메일",
565
- role: "권한",
566
- status: "상태",
567
- active: "활성",
568
- banned: "차단됨",
569
- makeUser: "일반 사용자로",
570
- makeAdmin: "관리자로",
571
- ban: "차단",
572
- unban: "차단 해제",
573
- empty: "회원이 없습니다.",
574
- total: "회원 {count}명",
575
- previous: "이전",
576
- next: "다음",
577
- roleSet: "권한을 {role}(으)로 변경했습니다",
578
- userBanned: "회원을 차단했습니다",
579
- userUnbanned: "차단을 해제했습니다",
580
- loadFailed: "회원 목록을 불러오지 못했습니다",
581
- actionFailed: "작업에 실패했습니다",
582
- addUser: "사용자 추가",
583
- createTitle: "사용자 생성",
584
- password: "비밀번호",
585
- create: "생성",
586
- cancel: "취소",
587
- close: "닫기",
588
- roleAdmin: "관리자",
589
- roleUser: "사용자",
590
- roleModerator: "중재자",
591
- userCreated: "사용자를 생성했습니다",
592
- setPassword: "비밀번호 설정",
593
- newPassword: "새 비밀번호",
594
- save: "저장",
595
- passwordSet: "비밀번호를 변경했습니다",
596
- delete: "삭제",
597
- deleteTitle: "사용자 삭제",
598
- deleteConfirm: "{email} 사용자를 삭제할까요? 되돌릴 수 없습니다.",
599
- userDeleted: "사용자를 삭제했습니다",
600
- confirmPassword: "비밀번호 확인",
601
- confirmNewPassword: "새 비밀번호 확인",
602
- passwordMismatch: "비밀번호가 일치하지 않습니다",
603
- edit: "편집",
604
- editTitle: "사용자 편집",
605
- userUpdated: "사용자를 수정했습니다",
606
- impersonate: "이 사용자로 로그인",
607
- banTitle: "사용자 차단",
608
- banReason: "사유 (선택)",
609
- banExpiryDays: "만료 (일, 선택)",
610
- banExpiry: "차단 만료",
611
- banPermanent: "영구 차단",
612
- joined: "가입일",
613
- stopImpersonating: "대리 로그인 종료",
614
- impersonatingAs: "{email} 사용자로 대리 로그인 중",
615
- manage: "관리",
616
- revokeSession: "종료",
617
- verified: "인증됨",
618
- unverified: "미인증",
619
- sendVerification: "인증 메일 보내기",
620
- verificationSent: "인증 메일을 보냈습니다",
621
- markVerified: "인증됨으로 표시",
622
- markedVerified: "인증 처리했습니다",
623
- sendResetEmail: "비밀번호 재설정 메일 보내기",
624
- resetEmailSent: "재설정 메일을 보냈습니다",
625
- sendVerificationOnCreate: "인증 메일 보내기",
626
- pendingApproval: "승인 대기",
627
- approve: "회원가입 승인",
628
- approved: "회원가입을 승인했습니다",
629
- },
630
- sessions: {
631
- title: "활성 세션",
632
- device: "기기",
633
- ip: "IP",
634
- since: "시작",
635
- revoke: "종료",
636
- unknown: "알 수 없음",
637
- empty: "활성 세션이 없습니다.",
638
- revoked: "세션을 종료했습니다",
639
- loadFailed: "세션을 불러오지 못했습니다",
640
- revokeFailed: "세션 종료에 실패했습니다",
641
- status: "상태",
642
- current: "현재",
643
- signOutOthers: "다른 세션 모두 종료",
644
- accountsTitle: "로그인된 계정", switchAccount: "전환", switchFailed: "계정 전환에 실패했습니다",
645
- othersRevoked: "다른 세션을 모두 종료했습니다",
646
- total: "세션 {count}개",
647
- },
648
- adminSessions: {
649
- title: "사용자 세션",
650
- user: "사용자",
651
- device: "기기",
652
- ip: "IP",
653
- since: "시작",
654
- expires: "만료",
655
- impersonated: "임퍼스네이트",
656
- revoke: "세션 종료",
657
- revokeAll: "사용자 세션 모두 종료",
658
- unknown: "알 수 없음",
659
- empty: "활성 세션이 없습니다.",
660
- revoked: "세션을 종료했습니다",
661
- allRevoked: "사용자의 세션을 모두 종료했습니다",
662
- loadFailed: "세션을 불러오지 못했습니다",
663
- total: "세션 {count}개",
664
- },
665
- account: {
666
- title: "계정",
667
- profile: "프로필",
668
- name: "이름",
669
- username: "사용자명",
670
- phone: "전화번호", phoneCode: "인증 코드", phoneSendCode: "코드 전송", phoneVerify: "인증", phoneCodeSent: "코드를 전송했습니다", phoneVerified: "전화번호 인증됨",
671
- email: "이메일",
672
- role: "권한",
673
- changePassword: "비밀번호 변경",
674
- currentPassword: "현재 비밀번호",
675
- newPassword: "새 비밀번호",
676
- update: "비밀번호 변경",
677
- updating: "변경 중…",
678
- changed: "비밀번호를 변경했습니다",
679
- changeFailed: "비밀번호 변경에 실패했습니다",
680
- nav: { profile: "프로필", security: "보안", connected: "연결", sessions: "세션", apiKeys: "API 키", danger: "위험 영역" },
681
- twoFactorTitle: "2단계 인증",
682
- twoFactorDesc: "로그인 시 인증 앱의 일회용 코드를 추가로 요구합니다.",
683
- twoFactorOn: "사용 중",
684
- twoFactorOff: "사용 안 함",
685
- enable: "사용",
686
- disable: "해제",
687
- password: "비밀번호",
688
- scanHint: "인증 앱에 등록한 뒤 6자리 코드를 입력하세요.",
689
- setupKey: "설정 키 (otpauth URI)",
690
- backupCodes: "백업 코드 — 안전한 곳에 보관하세요",
691
- backupCodesHint: "인증 앱을 분실했을 때 각 코드는 한 번씩 사용할 수 있습니다. 지금 저장하세요 — 다시 표시되지 않습니다.",
692
- downloadBackupCodes: "다운로드",
693
- regenerateBackupCodes: "백업 코드 재발급",
694
- regenerateBackupCodesHint: "새로 발급하면 이전 백업 코드는 무효화됩니다.",
695
- backupCodesRegenerated: "새 백업 코드를 발급했습니다",
696
- code: "인증 코드",
697
- verify: "확인",
698
- twoFactorEnabled: "2단계 인증을 사용합니다",
699
- twoFactorDisabled: "2단계 인증을 해제했습니다",
700
- connectedTitle: "연결된 계정",
701
- connectedDesc: "소셜 계정을 연결하면 해당 계정으로 로그인할 수 있습니다.",
702
- connect: "연결",
703
- disconnect: "연결 해제",
704
- connected: "연결됨",
705
- notConnected: "연결 안 됨",
706
- disconnected: "연결을 해제했습니다",
707
- dangerTitle: "계정 삭제",
708
- dangerDesc: "계정과 모든 관련 데이터를 영구히 삭제합니다.",
709
- deleteAccount: "계정 삭제",
710
- deleteAccountConfirm: "계정이 영구히 삭제되며 되돌릴 수 없습니다. 확인하려면 비밀번호를 입력하세요.",
711
- accountDeleted: "계정을 삭제했습니다",
712
- save: "변경 저장",
713
- saved: "프로필을 저장했습니다",
714
- saveFailed: "프로필 저장에 실패했습니다",
715
- saving: "저장 중…",
716
- changeEmail: "이메일 변경",
717
- emailChanged: "이메일 주소를 변경했습니다",
718
- emailChangePending: "현재 메일함에서 변경을 승인해 주세요",
719
- verified: "인증됨",
720
- unverified: "미인증",
721
- },
722
- // Modules add their i18n blocks here.
723
- // podokit:begin:messages-ko
724
- // podokit:end:messages-ko
725
- };
22
+ const definitionFiles = import.meta.glob<{ default: LocaleDefinition }>("./locales/*.json", {
23
+ eager: true,
24
+ });
25
+ const catalogFiles = import.meta.glob<{ default: JsonObject }>("./catalogs/*/*.json");
726
26
 
727
- export const messages: Record<Locale, Messages> = { en, ko };
27
+ function definitions(): LocaleDefinition[] {
28
+ return Object.values(definitionFiles)
29
+ .map((module) => module.default)
30
+ .filter((definition) => definition.enabled)
31
+ .sort((left, right) => left.code.localeCompare(right.code));
32
+ }
33
+
34
+ export const localeDefinitions = definitions();
35
+ export const LOCALES = localeDefinitions.map((definition) => definition.code);
36
+ export const defaultLocale: Locale = "en";
37
+ export const localeNames: Record<string, string> = Object.fromEntries(
38
+ localeDefinitions.map((definition) => [definition.code, definition.name]),
39
+ );
728
40
 
729
41
  export function resolveLocale(value: string | undefined | null): Locale {
730
- return LOCALES.includes(value as Locale) ? (value as Locale) : defaultLocale;
42
+ if (!value) return defaultLocale;
43
+ const exact = localeDefinitions.find((definition) => definition.code === value);
44
+ if (exact) return exact.code;
45
+ const insensitive = localeDefinitions.find(
46
+ (definition) => definition.code.toLowerCase() === value.toLowerCase(),
47
+ );
48
+ return insensitive?.code ?? defaultLocale;
49
+ }
50
+
51
+ function isObject(value: unknown): value is JsonObject {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+
55
+ function mergeCatalog(target: JsonObject, source: JsonObject): JsonObject {
56
+ const result: JsonObject = { ...target };
57
+ for (const [key, value] of Object.entries(source)) {
58
+ const previous = result[key];
59
+ result[key] = isObject(previous) && isObject(value) ? mergeCatalog(previous, value) : value;
60
+ }
61
+ return result;
62
+ }
63
+
64
+ function catalogCode(path: string): string | null {
65
+ const match = path.match(/\/([^/]+)\.json$/);
66
+ return match?.[1] ?? null;
67
+ }
68
+
69
+ async function loadCatalog(code: string): Promise<JsonObject> {
70
+ const paths = Object.keys(catalogFiles)
71
+ .filter((path) => catalogCode(path) === code)
72
+ .sort((left, right) => {
73
+ const leftApp = left.includes("/app/") ? 1 : 0;
74
+ const rightApp = right.includes("/app/") ? 1 : 0;
75
+ return leftApp - rightApp || left.localeCompare(right);
76
+ });
77
+ let catalog: JsonObject = {};
78
+ for (const path of paths) {
79
+ const loader = catalogFiles[path];
80
+ if (!loader) continue;
81
+ catalog = mergeCatalog(catalog, (await loader()).default);
82
+ }
83
+ return catalog;
84
+ }
85
+
86
+ export async function loadMessages(
87
+ locale: string,
88
+ configuredFallback?: string | null,
89
+ ): Promise<Messages> {
90
+ const selected = resolveLocale(locale);
91
+ const fallback = resolveLocale(configuredFallback);
92
+ const candidates = [defaultLocale, fallback, selected];
93
+ // A later catalog has higher priority. Keep the last occurrence so selecting
94
+ // the built-in English fallback still overrides a non-English site default.
95
+ const chain = candidates.filter((code, index) => candidates.lastIndexOf(code) === index);
96
+ let catalog: JsonObject = {};
97
+ for (const code of chain) catalog = mergeCatalog(catalog, await loadCatalog(code));
98
+ return catalog as Messages;
731
99
  }