diff --git a/bun.lock b/bun.lock
index 7cbc3dd93..057cfbc1d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -79,6 +79,7 @@
"jscpd": "^4.0.9",
"knip": "^6.11.0",
"madge": "^8.0.0",
+ "magic-string": "0.30.21",
"playwright-core": "^1.60.0",
"react-doctor": "^0.2.14",
"secretlint": "^12.3.1",
diff --git a/docs/README.md b/docs/README.md
index 2b1606817..6375eea2b 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -59,6 +59,7 @@ docs/
│ ├── architecture-tests.md ← catalog of every architecture gate
│ ├── editor-history.md ← patch-based undo/redo history
│ ├── react-compiler.md ← memoization rule, three exceptions, gates
+│ ├── admin-i18n.md ← full-admin translations, extraction + locale persistence
│ └── use-async-resource.md ← canonical async load hook; when to use vs. not
│
├── deployment/ ← operator docs (running the thing)
@@ -179,6 +180,7 @@ Three categories, three voices:
| [reference/architecture-tests.md](reference/architecture-tests.md) | Catalog of every architecture gate test |
| [reference/editor-history.md](reference/editor-history.md) | Patch-based undo/redo history: `HistoryEntry`, `mutate*` helpers, coalescing |
| [reference/react-compiler.md](reference/react-compiler.md) | React Compiler memoization rule, three exceptions, enforcement gates |
+| [reference/admin-i18n.md](reference/admin-i18n.md) | Full-admin English/Chinese catalogs, build-time extraction, switching, and persistence |
| [reference/use-async-resource.md](reference/use-async-resource.md) | `useAsyncResource` — canonical single-resource async load hook; when to use and when not to |
### Operations
@@ -221,4 +223,5 @@ Quick map from "where do I look for X?" to the canonical file:
| CSS value sanitiser | `src/core/css-sanitize/sanitiseCssValue.ts` |
| TypeBox helpers | `src/core/utils/typeboxHelpers.ts` |
| Error message extraction | `src/core/utils/errorMessage.ts` |
+| Admin translations | `src/admin/i18n/` + `scripts/lib/adminI18n.ts` |
| Architecture gate tests | `src/__tests__/architecture/*.test.ts` |
diff --git a/docs/editor.md b/docs/editor.md
index f10e1d0bf..ccd0a9559 100644
--- a/docs/editor.md
+++ b/docs/editor.md
@@ -9,6 +9,7 @@ The frontend is a single React 19 + Vite SPA mounted at `/admin`. Inside it, two
## TL;DR
- **Entry:** `src/admin/main.tsx` mounts `` with React 19 root-level error callbacks. `flushSync` forces the initial render synchronous to cut LCP.
+- **Admin locale:** `src/admin/i18n/` plus the Vite pre-transform provide complete English and Simplified Chinese coverage across every workspace. Simplified Chinese is the default; the persisted switch is available pre-auth and in the account menu. See [`reference/admin-i18n.md`](reference/admin-i18n.md).
- **Router:** `src/admin/lib/routing/` — in-house router replacing `react-router-dom`. Ten workspace/page routes are wrapped in a per-route `` and ``, with root redirects plus a final `path="/admin/*"` catch-all redirecting unknown admin URLs to `/admin/dashboard` (login form when unauthenticated) instead of rendering an empty tree. Public-site 404s are NOT claimed — the publish pipeline's NotFound handling owns those.
- **Cold path:** entry chunk is tiny. `AuthenticatedAdmin` is `React.lazy` and only loads post-login. Each workspace page is wrapped in `prewarmedLazy(...)`: the active page fires its import at module evaluation; the remaining pages pre-warm via `requestIdleCallback` after first paint so subsequent nav is synchronous (no Suspense flicker).
- **Workspaces:** `dashboard`, `site` (the editor), `content`, `data`, `media`, `plugins`, `users`, `ai`, `account`, `pluginPage`. Capability-gated by `canAccessWorkspace`.
diff --git a/docs/reference/admin-i18n.md b/docs/reference/admin-i18n.md
new file mode 100644
index 000000000..529d945af
--- /dev/null
+++ b/docs/reference/admin-i18n.md
@@ -0,0 +1,98 @@
+# Admin i18n
+
+Simplified Chinese and English localization for the complete admin application.
+
+The admin defaults to Simplified Chinese (`zh-CN`). An explicit language choice is persisted locally, updates ``, and is available before and after authentication. Published-site language remains a separate setting.
+
+---
+
+## TL;DR
+
+- `src/admin/i18n/catalog.ts` contains strongly typed, named messages used by authentication and application-level UI.
+- English source literals in admin components are gettext-style message IDs. `scripts/lib/adminI18n.ts` extracts supported user-facing contexts and the Vite pre-transform injects English plus Simplified Chinese at build time.
+- Simplified Chinese literal catalogs live in `src/admin/i18n/locales/zh-CN/`, split by workspace so the source remains reviewable.
+- `src/admin/i18n/runtime.ts` selects the already bundled literal for the active locale. Each route chunk carries only its own translated strings; the entry chunk does not import the complete catalog.
+- `I18nProvider` owns the locale, remounts its child tree after a language change, and synchronizes the document language.
+- `instatic-admin-locale-v1` is TypeBox-validated. With no saved preference, the admin always starts in Simplified Chinese.
+- `admin-i18n-coverage.test.ts` fails when a newly introduced admin message has no Simplified Chinese translation.
+
+## Architecture
+
+| Responsibility | Source of truth |
+|---|---|
+| Supported locales and named messages | `src/admin/i18n/catalog.ts` |
+| Workspace literal catalogs | `src/admin/i18n/locales/zh-CN/*.ts` |
+| Combined build-time catalog | `src/admin/i18n/literalCatalog.ts` |
+| Extraction and Vite transformation | `scripts/lib/adminI18n.ts` |
+| Runtime literal selection | `src/admin/i18n/runtime.ts` |
+| React context and document synchronization | `src/admin/i18n/I18nProvider.tsx` |
+| Persisted preference | `src/admin/i18n/localePreference.ts` |
+| Pre-auth switch | `src/admin/i18n/LanguageSwitcher.tsx` |
+| Authenticated switch | `src/admin/shared/AccountMenuButton/AccountMenuButton.tsx` |
+| Coverage gate | `src/__tests__/architecture/admin-i18n-coverage.test.ts` |
+
+Locale resolution is deliberately simple:
+
+```text
+validated localStorage preference
+ → Simplified Chinese default
+```
+
+The language control remains available on the setup/login screen and in the authenticated account menu. Changing it persists the preference and remounts the admin subtree so module-level configuration getters and all rendered literals resolve against one locale.
+
+## Two message forms
+
+Use a named message for application infrastructure, messages shared across unrelated contexts, or interpolation that benefits from a semantic key:
+
+```tsx
+import { useI18n } from '@admin/i18n'
+
+export function Example() {
+ const { t } = useI18n()
+ return
{t('preauth.setup.title')}
+}
+```
+
+Regular admin component copy stays readable in place:
+
+```tsx
+
+```
+
+The build transform recognizes visible JSX text, accessible text attributes, render-time string branches, selected UI configuration properties such as `title`, `label`, and `description`, and user-message setters. It rewrites only messages present in the Chinese catalog, while the coverage gate ensures the recognized set is complete.
+
+Parameterized template literals use positional placeholders in the literal catalog:
+
+```ts
+"{0} items": "{0} 项"
+```
+
+Run the report while migrating or reviewing copy:
+
+```bash
+bun scripts/admin-i18n-report.ts --missing
+bun scripts/admin-i18n-report.ts --area=site --missing
+```
+
+## Adding or changing copy
+
+1. Write the English UI copy in its component or named catalog.
+2. Run `bun scripts/admin-i18n-report.ts --missing`.
+3. Add each missing literal to the appropriate `src/admin/i18n/locales/zh-CN/` catalog.
+4. Run the architecture gate and build. The production build is the authoritative transform check.
+
+Do not translate user content, site names, plugin-provided labels, URLs, source code, CSS values, or internal identifiers. The extractor intentionally limits itself to known UI contexts rather than rewriting arbitrary string literals.
+
+## Adding a locale
+
+1. Add the locale tag and named catalog to `src/admin/i18n/catalog.ts`.
+2. Extend `AdminLocalePreferenceSchema` in `localePreference.ts`.
+3. Add a literal catalog for the locale and pass it to the build transform.
+4. Add the locale to both language controls.
+5. Cover persistence, named interpolation, literal transformation, and a rendered page in tests.
+
+## Related
+
+- `docs/editor.md` — admin SPA boot and provider placement.
+- `docs/reference/persistence-keys.md` — client preference key catalog.
+- `docs/reference/typebox-patterns.md` — persisted-boundary validation.
diff --git a/docs/reference/architecture-tests.md b/docs/reference/architecture-tests.md
index 05738be2f..acd0a24bc 100644
--- a/docs/reference/architecture-tests.md
+++ b/docs/reference/architecture-tests.md
@@ -6,7 +6,7 @@ Catalog of every test in `src/__tests__/architecture/`. These are structural gat
## TL;DR
-- 95 gate files across structural domains: SQL, JSON columns, migrations, CSS, icons, primitives, page tree, sandbox, agent, router, content storage, boundary validation, module size, AI, auth, error handling, etc.
+- 96 gate files across structural domains: SQL, JSON columns, migrations, CSS, icons, primitives, page tree, sandbox, agent, router, admin localization, content storage, boundary validation, module size, AI, auth, error handling, etc.
- Naming convention: `.test.ts` (kebab-case) or `-.test.ts`. A few legacy `task-*` ids remain for live invariants; new gates should use topic names.
- Run them all: `bun test src/__tests__/architecture/`.
- Most are **import / source scans** — they parse the files in scope and assert / reject patterns. Some are unit-style (a small in-test database, a synthesized page tree).
@@ -121,6 +121,7 @@ See [docs/reference/ui-primitives.md](ui-primitives.md).
| `no-circular-dependencies.test.ts` | `madge` finds zero tsconfig-aware circular dependencies across `src` and `server`. |
| `canvas-aware-selectors.test.ts` | Canvas-related store selectors are subscribed correctly to canvas-state slices. |
| `admin-router-usage.test.ts` | Internal admin navigation uses `@admin/lib/routing`; raw `/admin` anchors and `react-router-dom` are banned. |
+| `admin-i18n-coverage.test.ts` | Every extracted user-facing admin literal has a Simplified Chinese translation, and the compile-time transform preserves intentionally empty translations used for suffix removal. |
| `framework-typography-spacing.test.ts` | The site framework's typography / spacing tokens compile correctly. |
| `component-system-placement.test.ts` | Every VC insertion flow (toolbar picker, context menu) routes through `insertComponentRef`; Site Explorer must not expose a component-to-canvas drag source, and direct `insertNode`/`addNodeToVc` with `'base.visual-component-ref'` is forbidden in placement files. |
| `task414-wrap-to-container.test.ts` | Wrap-to-container action creates defaulted wrappers and preserves tree structure. |
diff --git a/docs/reference/persistence-keys.md b/docs/reference/persistence-keys.md
index ded530988..509748838 100644
--- a/docs/reference/persistence-keys.md
+++ b/docs/reference/persistence-keys.md
@@ -19,6 +19,7 @@ Catalog of every `localStorage` / `sessionStorage` key the admin app writes, and
| Key | Owner | Source-of-truth file |
|-------------------------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------|
+| `instatic-admin-locale-v1` | Explicit admin UI locale selected with the language switcher; absent means Simplified Chinese | `src/admin/i18n/localePreference.ts` → `ADMIN_LOCALE_STORAGE_KEY` |
| `instatic-editor-prefs` | All editor preferences (auto-save, hover-preview, admin theme, UI text size, density, layers options) — see [docs/features/editor-preferences.md](../features/editor-preferences.md) | `src/admin/pages/site/preferences/editorPreferences.ts` → `EDITOR_PREFS_KEY` |
| `instatic-editor-layout-v2` | Per-workspace sidebar widths + open states (site / content / data / media) and floating panel positions | `src/admin/state/workspaceLayoutStorage.ts` → `EDITOR_LAYOUT_STORAGE_KEY` |
| `instatic-clipboard-v1` | The editor clipboard (copy / cut / paste of layer subtrees) | `src/admin/pages/site/store/clipboard/clipboardStorage.ts` → `CLIPBOARD_STORAGE_KEY` |
diff --git a/package.json b/package.json
index fe9a50d9b..2b9a3ec9a 100644
--- a/package.json
+++ b/package.json
@@ -132,6 +132,7 @@
"jscpd": "^4.0.9",
"knip": "^6.11.0",
"madge": "^8.0.0",
+ "magic-string": "0.30.21",
"playwright-core": "^1.60.0",
"react-doctor": "^0.2.14",
"secretlint": "^12.3.1",
diff --git a/scripts/admin-i18n-report.ts b/scripts/admin-i18n-report.ts
new file mode 100644
index 000000000..d1935f5d5
--- /dev/null
+++ b/scripts/admin-i18n-report.ts
@@ -0,0 +1,50 @@
+import { readFile } from 'node:fs/promises'
+import { extractAdminMessages } from './lib/adminI18n'
+import { adminLiteralZhCN } from '../src/admin/i18n/literalCatalog'
+
+const requestedArea = Bun.argv.find((argument) => argument.startsWith('--area='))?.slice(7)
+const missingOnly = Bun.argv.includes('--missing')
+const occurrences = []
+
+function areaFor(filePath: string): string {
+ const pageMatch = filePath.match(/^src\/admin\/pages\/([^/]+)\//)
+ if (pageMatch?.[1]) return pageMatch[1]
+ if (filePath.startsWith('src/admin/modals/')) return 'modals'
+ if (filePath.startsWith('src/admin/spotlight/')) return 'spotlight'
+ return 'shared'
+}
+
+const glob = new Bun.Glob('src/admin/**/*.{ts,tsx}')
+for await (const filePath of glob.scan({ cwd: process.cwd(), onlyFiles: true })) {
+ if (
+ filePath.includes('/__tests__/') ||
+ filePath.includes('/i18n/') ||
+ filePath.endsWith('.test.ts') ||
+ filePath.endsWith('.test.tsx')
+ ) {
+ continue
+ }
+ const area = areaFor(filePath)
+ if (requestedArea && requestedArea !== area) continue
+ const source = await readFile(filePath, 'utf8')
+ occurrences.push(...extractAdminMessages(source, filePath).map((item) => ({ ...item, area })))
+}
+
+const messages = new Map()
+for (const item of occurrences) {
+ const current = messages.get(item.message)
+ const reference = `${item.filePath}:${item.line}`
+ if (current) {
+ if (!current.references.includes(reference)) current.references.push(reference)
+ } else {
+ messages.set(item.message, { area: item.area, references: [reference] })
+ }
+}
+
+const rows = [...messages.entries()].sort(([left], [right]) => left.localeCompare(right))
+for (const [message, details] of rows) {
+ if (missingOnly && message in adminLiteralZhCN) continue
+ process.stdout.write(`${JSON.stringify(message)}\t${details.area}\t${details.references.join(',')}\n`)
+}
+const missingCount = rows.filter(([message]) => !(message in adminLiteralZhCN)).length
+process.stderr.write(`${rows.length} unique messages, ${missingCount} missing across ${occurrences.length} occurrences\n`)
diff --git a/scripts/lib/adminI18n.ts b/scripts/lib/adminI18n.ts
new file mode 100644
index 000000000..3866e4df2
--- /dev/null
+++ b/scripts/lib/adminI18n.ts
@@ -0,0 +1,656 @@
+import MagicString from 'magic-string'
+import * as ts from 'typescript'
+import type { Plugin } from 'vite'
+
+export interface AdminLiteralCatalog {
+ readonly [englishMessage: string]: string
+}
+
+export interface AdminMessageOccurrence {
+ filePath: string
+ line: number
+ message: string
+ kind: 'jsx-text' | 'jsx-attribute' | 'jsx-expression' | 'object-property' | 'call-argument'
+}
+
+const LOCALIZE_IMPORT =
+ "import { localizeAdminLiteral as __instaticAdminLocalize, formatAdminLiteral as __instaticAdminFormat } from '@admin/i18n/runtime'\n"
+
+const USER_FACING_ATTRIBUTES = new Set([
+ 'alt',
+ 'aria-description',
+ 'aria-label',
+ 'ariaLabel',
+ 'caption',
+ 'content',
+ 'description',
+ 'emptyLabel',
+ 'emptyMessage',
+ 'errorMessage',
+ 'helperText',
+ 'hint',
+ 'label',
+ 'placeholder',
+ 'subtitle',
+ 'sub',
+ 'successMessage',
+ 'title',
+ 'tooltip',
+])
+
+const USER_FACING_PROPERTIES = new Set([
+ 'actionLabel',
+ 'ariaLabel',
+ 'body',
+ 'cancelLabel',
+ 'caption',
+ 'cta',
+ 'desc',
+ 'confirmLabel',
+ 'description',
+ 'detail',
+ 'emptyLabel',
+ 'emptyMessage',
+ 'errorMessage',
+ 'heading',
+ 'helperText',
+ 'hint',
+ 'label',
+ 'message',
+ 'placeholder',
+ 'submitLabel',
+ 'subtitle',
+ 'successMessage',
+ 'summary',
+ 'title',
+ 'tooltip',
+ 'valueLabel',
+])
+
+const USER_MESSAGE_CALLS = new Set([
+ 'getErrorMessage',
+ 'setError',
+ 'setErrorMessage',
+ 'setMessage',
+ 'setStatus',
+ 'setStatusMessage',
+])
+
+const USER_FACING_NAME =
+ /greeting|(?:ariaLabel|caption|cta|desc|description|label|message|placeholder|relative|status|text|title|tooltip|verb)$/i
+
+interface LiteralMessage {
+ message: string
+ english: string
+ expressions: string[]
+}
+
+interface Replacement {
+ start: number
+ end: number
+ text: string
+}
+
+function scriptKind(filePath: string): ts.ScriptKind {
+ return filePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
+}
+
+function propertyName(node: ts.PropertyName): string | null {
+ if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
+ return node.text
+ }
+ return null
+}
+
+function callName(expression: ts.Expression): string | null {
+ if (ts.isIdentifier(expression)) return expression.text
+ if (ts.isPropertyAccessExpression(expression)) return expression.name.text
+ return null
+}
+
+function declarationName(node: ts.BindingName | undefined): string | null {
+ return node && ts.isIdentifier(node) ? node.text : null
+}
+
+function cleanJsxText(raw: string): string {
+ const lines = raw.split(/\r\n|\n|\r/)
+ let lastNonEmptyLine = 0
+ for (let index = 0; index < lines.length; index += 1) {
+ if (/[^ \t]/.test(lines[index] ?? '')) lastNonEmptyLine = index
+ }
+
+ let result = ''
+ for (let index = 0; index < lines.length; index += 1) {
+ let line = (lines[index] ?? '').replace(/\t/g, ' ')
+ if (index !== 0) line = line.replace(/^ +/, '')
+ if (index !== lines.length - 1) line = line.replace(/ +$/, '')
+ if (!line) continue
+ if (index !== lastNonEmptyLine) line += ' '
+ result += line
+ }
+ return result
+}
+
+function isCandidateMessage(message: string): boolean {
+ const normalized = message.trim()
+ if (!normalized || !/[A-Za-z]/.test(normalized)) return false
+ if (/^(https?:|data:|\/|\.\/|\.\.\/|--|#[0-9a-f]{3,8}$)/i.test(normalized)) return false
+ if (/^[a-z][a-z0-9]*(?:\.[a-z0-9]+)+$/.test(normalized)) return false
+ return true
+}
+
+function literalMessage(
+ node: ts.StringLiteralLike | ts.TemplateExpression,
+ source: string,
+): LiteralMessage | null {
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
+ if (!isCandidateMessage(node.text)) return null
+ return { message: node.text.trim(), english: node.text, expressions: [] }
+ }
+
+ if (!ts.isTemplateExpression(node)) return null
+
+ let message = node.head.text
+ const expressions: string[] = []
+ for (let index = 0; index < node.templateSpans.length; index += 1) {
+ const span = node.templateSpans[index]
+ if (!span) continue
+ message += `{${index}}${span.literal.text}`
+ expressions.push(source.slice(span.expression.getStart(), span.expression.end))
+ }
+ if (!isCandidateMessage(message)) return null
+ return { message: message.trim(), english: message, expressions }
+}
+
+function translatedCall(literal: LiteralMessage, chinese: string): string {
+ if (literal.expressions.length === 0) {
+ return `__instaticAdminLocalize(${JSON.stringify(literal.english)}, ${JSON.stringify(chinese)})`
+ }
+ return `__instaticAdminFormat(${JSON.stringify(literal.english)}, ${JSON.stringify(chinese)}, [${literal.expressions.join(', ')}])`
+}
+
+function collectValueLiterals(
+ node: ts.Node,
+ source: string,
+ catalog: AdminLiteralCatalog,
+): Replacement[] {
+ const replacements: Replacement[] = []
+
+ function visit(current: ts.Node): void {
+ if (
+ ts.isStringLiteral(current) ||
+ ts.isNoSubstitutionTemplateLiteral(current) ||
+ ts.isTemplateExpression(current)
+ ) {
+ const literal = literalMessage(current, source)
+ const chinese = literal && literal.message in catalog ? catalog[literal.message] : undefined
+ if (literal && chinese !== undefined) {
+ replacements.push({
+ start: current.getStart(),
+ end: current.end,
+ text: translatedCall(literal, chinese),
+ })
+ }
+ return
+ }
+
+ if (
+ ts.isParenthesizedExpression(current) ||
+ ts.isAsExpression(current) ||
+ ts.isTypeAssertionExpression(current) ||
+ ts.isNonNullExpression(current)
+ ) {
+ visit(current.expression)
+ return
+ }
+ if (ts.isConditionalExpression(current)) {
+ visit(current.whenTrue)
+ visit(current.whenFalse)
+ return
+ }
+ if (ts.isBinaryExpression(current)) {
+ const token = current.operatorToken.kind
+ if (
+ token === ts.SyntaxKind.PlusToken ||
+ token === ts.SyntaxKind.BarBarToken ||
+ token === ts.SyntaxKind.QuestionQuestionToken ||
+ token === ts.SyntaxKind.AmpersandAmpersandToken
+ ) {
+ visit(current.left)
+ visit(current.right)
+ }
+ return
+ }
+ if (ts.isArrayLiteralExpression(current)) {
+ for (const element of current.elements) visit(element)
+ }
+ }
+
+ visit(node)
+ return replacements
+}
+
+function collectRenderableOccurrences(
+ node: ts.Node,
+ source: string,
+ sourceFile: ts.SourceFile,
+ filePath: string,
+ kind: AdminMessageOccurrence['kind'],
+ output: AdminMessageOccurrence[],
+): void {
+ if (
+ ts.isStringLiteral(node) ||
+ ts.isNoSubstitutionTemplateLiteral(node) ||
+ ts.isTemplateExpression(node)
+ ) {
+ const literal = literalMessage(node, source)
+ if (literal) output.push(occurrence(sourceFile, filePath, node, literal.message, kind))
+ return
+ }
+ if (
+ ts.isParenthesizedExpression(node) ||
+ ts.isAsExpression(node) ||
+ ts.isTypeAssertionExpression(node) ||
+ ts.isNonNullExpression(node)
+ ) {
+ collectRenderableOccurrences(node.expression, source, sourceFile, filePath, kind, output)
+ return
+ }
+ if (ts.isConditionalExpression(node)) {
+ collectRenderableOccurrences(node.whenTrue, source, sourceFile, filePath, kind, output)
+ collectRenderableOccurrences(node.whenFalse, source, sourceFile, filePath, kind, output)
+ return
+ }
+ if (ts.isBinaryExpression(node)) {
+ const token = node.operatorToken.kind
+ if (
+ token === ts.SyntaxKind.PlusToken ||
+ token === ts.SyntaxKind.BarBarToken ||
+ token === ts.SyntaxKind.QuestionQuestionToken ||
+ token === ts.SyntaxKind.AmpersandAmpersandToken
+ ) {
+ collectRenderableOccurrences(node.left, source, sourceFile, filePath, kind, output)
+ collectRenderableOccurrences(node.right, source, sourceFile, filePath, kind, output)
+ }
+ return
+ }
+}
+
+function collectReturnOccurrences(
+ body: ts.ConciseBody,
+ source: string,
+ sourceFile: ts.SourceFile,
+ filePath: string,
+ output: AdminMessageOccurrence[],
+): void {
+ function visit(node: ts.Node): void {
+ if (ts.isReturnStatement(node) && node.expression) {
+ collectRenderableOccurrences(
+ node.expression,
+ source,
+ sourceFile,
+ filePath,
+ 'call-argument',
+ output,
+ )
+ return
+ }
+ if (node !== body && (ts.isFunctionLike(node) || ts.isClassLike(node))) return
+ ts.forEachChild(node, visit)
+ }
+
+ if (ts.isBlock(body)) visit(body)
+ else collectRenderableOccurrences(body, source, sourceFile, filePath, 'call-argument', output)
+}
+
+function collectReturnReplacements(
+ body: ts.ConciseBody,
+ source: string,
+ catalog: AdminLiteralCatalog,
+): Replacement[] {
+ const replacements: Replacement[] = []
+ function visit(node: ts.Node): void {
+ if (ts.isReturnStatement(node) && node.expression) {
+ replacements.push(...collectValueLiterals(node.expression, source, catalog))
+ return
+ }
+ if (node !== body && (ts.isFunctionLike(node) || ts.isClassLike(node))) return
+ ts.forEachChild(node, visit)
+ }
+
+ if (ts.isBlock(body)) visit(body)
+ else replacements.push(...collectValueLiterals(body, source, catalog))
+ return replacements
+}
+
+function applyRelativeReplacements(
+ source: string,
+ start: number,
+ end: number,
+ replacements: readonly Replacement[],
+): string {
+ let output = source.slice(start, end)
+ const descending = [...replacements].sort((left, right) => right.start - left.start)
+ for (const replacement of descending) {
+ const relativeStart = replacement.start - start
+ const relativeEnd = replacement.end - start
+ output = output.slice(0, relativeStart) + replacement.text + output.slice(relativeEnd)
+ }
+ return output
+}
+
+function occurrence(
+ sourceFile: ts.SourceFile,
+ filePath: string,
+ node: ts.Node,
+ message: string,
+ kind: AdminMessageOccurrence['kind'],
+): AdminMessageOccurrence {
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
+ return { filePath, line: position.line + 1, message, kind }
+}
+
+export function extractAdminMessages(source: string, filePath: string): AdminMessageOccurrence[] {
+ const sourceFile = ts.createSourceFile(
+ filePath,
+ source,
+ ts.ScriptTarget.Latest,
+ true,
+ scriptKind(filePath),
+ )
+ const occurrences: AdminMessageOccurrence[] = []
+
+ function visit(node: ts.Node): void {
+ if (ts.isJsxText(node)) {
+ const message = cleanJsxText(node.getFullText(sourceFile)).trim()
+ if (isCandidateMessage(message)) {
+ occurrences.push(occurrence(sourceFile, filePath, node, message, 'jsx-text'))
+ }
+ return
+ }
+
+ if (ts.isJsxAttribute(node)) {
+ const name = node.name.getText(sourceFile)
+ if (USER_FACING_ATTRIBUTES.has(name) && node.initializer) {
+ if (ts.isStringLiteral(node.initializer)) {
+ const literal = literalMessage(node.initializer, source)
+ if (literal) {
+ occurrences.push(occurrence(sourceFile, filePath, node.initializer, literal.message, 'jsx-attribute'))
+ }
+ } else if (ts.isJsxExpression(node.initializer) && node.initializer.expression) {
+ collectRenderableOccurrences(
+ node.initializer.expression,
+ source,
+ sourceFile,
+ filePath,
+ 'jsx-attribute',
+ occurrences,
+ )
+ }
+ }
+ if (node.initializer) visitNestedUiNodes(node.initializer)
+ return
+ }
+
+ if (ts.isJsxExpression(node) && node.expression) {
+ collectRenderableOccurrences(
+ node.expression,
+ source,
+ sourceFile,
+ filePath,
+ 'jsx-expression',
+ occurrences,
+ )
+ visitNestedUiNodes(node.expression)
+ return
+ }
+
+ if (ts.isFunctionDeclaration(node)) {
+ const name = node.name?.text
+ if (name && USER_FACING_NAME.test(name) && node.body) {
+ collectReturnOccurrences(node.body, source, sourceFile, filePath, occurrences)
+ visitNestedUiNodes(node.body)
+ return
+ }
+ }
+
+ if (ts.isVariableDeclaration(node)) {
+ const name = declarationName(node.name)
+ if (name && USER_FACING_NAME.test(name) && node.initializer) {
+ if (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) {
+ collectReturnOccurrences(
+ node.initializer.body,
+ source,
+ sourceFile,
+ filePath,
+ occurrences,
+ )
+ } else {
+ collectRenderableOccurrences(
+ node.initializer,
+ source,
+ sourceFile,
+ filePath,
+ 'call-argument',
+ occurrences,
+ )
+ }
+ visitNestedUiNodes(node.initializer)
+ return
+ }
+ }
+
+ if (ts.isPropertyAssignment(node)) {
+ const name = propertyName(node.name)
+ if (name && USER_FACING_PROPERTIES.has(name)) {
+ collectRenderableOccurrences(
+ node.initializer,
+ source,
+ sourceFile,
+ filePath,
+ 'object-property',
+ occurrences,
+ )
+ return
+ }
+ }
+
+ if (ts.isCallExpression(node)) {
+ const name = callName(node.expression)
+ if (name && USER_MESSAGE_CALLS.has(name)) {
+ for (const argument of node.arguments) {
+ collectRenderableOccurrences(
+ argument,
+ source,
+ sourceFile,
+ filePath,
+ 'call-argument',
+ occurrences,
+ )
+ }
+ return
+ }
+ }
+
+ ts.forEachChild(node, visit)
+ }
+
+ function visitNestedUiNodes(node: ts.Node): void {
+ if (
+ ts.isJsxElement(node) ||
+ ts.isJsxFragment(node) ||
+ ts.isJsxSelfClosingElement(node) ||
+ (ts.isPropertyAssignment(node) && USER_FACING_PROPERTIES.has(propertyName(node.name) ?? ''))
+ ) {
+ visit(node)
+ return
+ }
+ ts.forEachChild(node, visitNestedUiNodes)
+ }
+
+ visit(sourceFile)
+ return occurrences
+}
+
+export function transformAdminMessages(
+ source: string,
+ filePath: string,
+ catalog: AdminLiteralCatalog,
+): { code: string; map: ReturnType } | null {
+ const sourceFile = ts.createSourceFile(
+ filePath,
+ source,
+ ts.ScriptTarget.Latest,
+ true,
+ scriptKind(filePath),
+ )
+ const output = new MagicString(source)
+ let replacementCount = 0
+
+ function replace(start: number, end: number, text: string): void {
+ output.overwrite(start, end, text)
+ replacementCount += 1
+ }
+
+ function visit(node: ts.Node): void {
+ if (ts.isJsxText(node)) {
+ const english = cleanJsxText(node.getFullText(sourceFile))
+ const message = english.trim()
+ const chinese = message in catalog ? catalog[message] : undefined
+ if (chinese !== undefined) replace(node.getFullStart(), node.end, `{${translatedCall({ message, english, expressions: [] }, chinese)}}`)
+ return
+ }
+
+ if (ts.isJsxAttribute(node)) {
+ const name = node.name.getText(sourceFile)
+ if (USER_FACING_ATTRIBUTES.has(name) && node.initializer) {
+ if (ts.isStringLiteral(node.initializer)) {
+ const literal = literalMessage(node.initializer, source)
+ const chinese = literal && literal.message in catalog ? catalog[literal.message] : undefined
+ if (literal && chinese !== undefined) {
+ replace(node.initializer.getStart(), node.initializer.end, `{${translatedCall(literal, chinese)}}`)
+ }
+ } else if (ts.isJsxExpression(node.initializer) && node.initializer.expression) {
+ const replacements = collectValueLiterals(node.initializer.expression, source, catalog)
+ for (const replacement of replacements) {
+ replace(replacement.start, replacement.end, replacement.text)
+ }
+ }
+ }
+ if (node.initializer) visitNestedUiNodes(node.initializer)
+ return
+ }
+
+ if (ts.isJsxExpression(node) && node.expression) {
+ const replacements = collectValueLiterals(node.expression, source, catalog)
+ for (const replacement of replacements) {
+ replace(replacement.start, replacement.end, replacement.text)
+ }
+ visitNestedUiNodes(node.expression)
+ return
+ }
+
+ if (ts.isFunctionDeclaration(node)) {
+ const name = node.name?.text
+ if (name && USER_FACING_NAME.test(name) && node.body) {
+ for (const replacement of collectReturnReplacements(node.body, source, catalog)) {
+ replace(replacement.start, replacement.end, replacement.text)
+ }
+ visitNestedUiNodes(node.body)
+ return
+ }
+ }
+
+ if (ts.isVariableDeclaration(node)) {
+ const name = declarationName(node.name)
+ if (name && USER_FACING_NAME.test(name) && node.initializer) {
+ const replacements =
+ ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)
+ ? collectReturnReplacements(node.initializer.body, source, catalog)
+ : collectValueLiterals(node.initializer, source, catalog)
+ for (const replacement of replacements) {
+ replace(replacement.start, replacement.end, replacement.text)
+ }
+ visitNestedUiNodes(node.initializer)
+ return
+ }
+ }
+
+ if (ts.isPropertyAssignment(node)) {
+ const name = propertyName(node.name)
+ if (name && USER_FACING_PROPERTIES.has(name)) {
+ const replacements = collectValueLiterals(node.initializer, source, catalog)
+ if (replacements.length > 0) {
+ const initializer = applyRelativeReplacements(
+ source,
+ node.initializer.getStart(),
+ node.initializer.end,
+ replacements,
+ )
+ const propertySource = source.slice(node.name.getStart(), node.name.end)
+ replace(node.getStart(), node.end, `get ${propertySource}() { return ${initializer} }`)
+ }
+ return
+ }
+ }
+
+ if (ts.isCallExpression(node)) {
+ const name = callName(node.expression)
+ if (name && USER_MESSAGE_CALLS.has(name)) {
+ for (const argument of node.arguments) {
+ const replacements = collectValueLiterals(argument, source, catalog)
+ for (const replacement of replacements) {
+ replace(replacement.start, replacement.end, replacement.text)
+ }
+ }
+ return
+ }
+ }
+
+ ts.forEachChild(node, visit)
+ }
+
+ function visitNestedUiNodes(node: ts.Node): void {
+ if (
+ ts.isJsxElement(node) ||
+ ts.isJsxFragment(node) ||
+ ts.isJsxSelfClosingElement(node) ||
+ (ts.isPropertyAssignment(node) && USER_FACING_PROPERTIES.has(propertyName(node.name) ?? ''))
+ ) {
+ visit(node)
+ return
+ }
+ ts.forEachChild(node, visitNestedUiNodes)
+ }
+
+ visit(sourceFile)
+ if (replacementCount === 0) return null
+
+ output.prepend(LOCALIZE_IMPORT)
+ return {
+ code: output.toString(),
+ map: output.generateMap({ hires: true, source: filePath, includeContent: true }),
+ }
+}
+
+function isAdminSource(filePath: string): boolean {
+ const normalized = filePath.replace(/\\/g, '/')
+ return (
+ normalized.includes('/src/admin/') &&
+ (normalized.endsWith('.ts') || normalized.endsWith('.tsx')) &&
+ !normalized.includes('/src/admin/i18n/') &&
+ !normalized.includes('/__tests__/')
+ )
+}
+
+export function adminI18nPlugin(catalog: AdminLiteralCatalog): Plugin {
+ return {
+ name: 'instatic-admin-i18n',
+ enforce: 'pre',
+ transform(source, rawId) {
+ const filePath = rawId.split('?')[0] ?? rawId
+ if (!isAdminSource(filePath)) return null
+ return transformAdminMessages(source, filePath, catalog)
+ },
+ }
+}
diff --git a/src/__tests__/admin/i18n.test.tsx b/src/__tests__/admin/i18n.test.tsx
new file mode 100644
index 000000000..24f23b22d
--- /dev/null
+++ b/src/__tests__/admin/i18n.test.tsx
@@ -0,0 +1,69 @@
+import { beforeEach, describe, expect, it } from 'bun:test'
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { afterEach } from 'bun:test'
+import { AdminPreAuthForm } from '@admin/preauth/AdminPreAuthForm'
+import {
+ ADMIN_LOCALE_STORAGE_KEY,
+ I18nProvider,
+ readAdminLocalePreference,
+ resolveInitialAdminLocale,
+ translate,
+ writeAdminLocalePreference,
+} from '@admin/i18n'
+
+beforeEach(() => {
+ localStorage.clear()
+ document.documentElement.lang = 'en'
+})
+
+afterEach(cleanup)
+
+describe('admin i18n', () => {
+ it('defaults to Simplified Chinese until the user chooses a locale', () => {
+ expect(resolveInitialAdminLocale()).toBe('zh-CN')
+ writeAdminLocalePreference('en')
+ expect(resolveInitialAdminLocale()).toBe('en')
+ })
+
+ it('formats translated messages with parameters', () => {
+ expect(translate('zh-CN', 'preauth.error.passwordTooShort', { min: 12 }))
+ .toBe('密码至少需要 12 个字符')
+ })
+
+ it('validates persisted locale preferences', () => {
+ writeAdminLocalePreference('zh-CN')
+ expect(readAdminLocalePreference()).toBe('zh-CN')
+
+ localStorage.setItem(ADMIN_LOCALE_STORAGE_KEY, JSON.stringify({ locale: 'de' }))
+ expect(readAdminLocalePreference()).toBeNull()
+
+ localStorage.setItem(ADMIN_LOCALE_STORAGE_KEY, '{invalid json')
+ expect(readAdminLocalePreference()).toBeNull()
+ })
+
+ it('renders and switches the setup flow in place', () => {
+ render(
+
+ {}}
+ onAuthenticated={() => {}}
+ />
+ ,
+ )
+
+ expect(screen.getByRole('heading', { name: '初始化 CMS' })).toBeTruthy()
+ expect(screen.getByRole('textbox', { name: '站点名称' }).getAttribute('value')).toBe('我的站点')
+ expect(screen.getByRole('button', { name: '创建管理员' })).toBeTruthy()
+ expect(document.documentElement.lang).toBe('zh-CN')
+
+ fireEvent.click(screen.getByRole('button', { name: '切换语言为English' }))
+
+ expect(screen.getByRole('heading', { name: 'Set Up CMS' })).toBeTruthy()
+ expect(screen.getByRole('button', { name: 'Create Admin' })).toBeTruthy()
+ expect(readAdminLocalePreference()).toBe('en')
+ expect(document.documentElement.lang).toBe('en')
+ })
+})
diff --git a/src/__tests__/app/appLoadingScreen.test.tsx b/src/__tests__/app/appLoadingScreen.test.tsx
index 5415a79bb..c64a53e7a 100644
--- a/src/__tests__/app/appLoadingScreen.test.tsx
+++ b/src/__tests__/app/appLoadingScreen.test.tsx
@@ -1,12 +1,17 @@
import { afterEach, describe, expect, it } from 'bun:test'
import { cleanup, render, screen } from '@testing-library/react'
import { AppLoadingScreen } from '@admin/AppLoadingScreen'
+import { I18nProvider } from '@admin/i18n'
afterEach(cleanup)
describe('AppLoadingScreen', () => {
it('renders one accessible centered loader without visible raw loading text or skeleton chrome', () => {
- render()
+ render(
+
+
+ ,
+ )
const status = screen.getByRole('status', { name: /loading instatic/i })
expect(status.getAttribute('aria-busy')).toBe('true')
diff --git a/src/__tests__/architecture/admin-i18n-coverage.test.ts b/src/__tests__/architecture/admin-i18n-coverage.test.ts
new file mode 100644
index 000000000..a64d1c169
--- /dev/null
+++ b/src/__tests__/architecture/admin-i18n-coverage.test.ts
@@ -0,0 +1,66 @@
+import { describe, expect, it } from 'bun:test'
+import { readFileSync, readdirSync } from 'node:fs'
+import { join, relative } from 'node:path'
+import {
+ extractAdminMessages,
+ transformAdminMessages,
+ type AdminMessageOccurrence,
+} from '../../../scripts/lib/adminI18n'
+import { adminLiteralZhCN } from '../../admin/i18n/literalCatalog'
+
+const REPO_ROOT = join(import.meta.dir, '../../..')
+const ADMIN_ROOT = join(REPO_ROOT, 'src/admin')
+
+function adminSourceFiles(): string[] {
+ return readdirSync(ADMIN_ROOT, { recursive: true, encoding: 'utf8' })
+ .filter((filePath) => filePath.endsWith('.ts') || filePath.endsWith('.tsx'))
+ .filter((filePath) => !filePath.includes('/__tests__/'))
+ .filter((filePath) => !filePath.startsWith('i18n/'))
+ .filter((filePath) => !filePath.endsWith('.test.ts') && !filePath.endsWith('.test.tsx'))
+ .map((filePath) => join(ADMIN_ROOT, filePath))
+}
+
+function allOccurrences(): AdminMessageOccurrence[] {
+ return adminSourceFiles().flatMap((absolutePath) => {
+ const filePath = relative(REPO_ROOT, absolutePath)
+ return extractAdminMessages(readFileSync(absolutePath, 'utf8'), filePath)
+ })
+}
+
+describe('admin i18n architecture', () => {
+ it('has a Simplified Chinese translation for every extracted admin message', () => {
+ const missing = allOccurrences()
+ .filter(({ message }) => !(message in adminLiteralZhCN))
+ .map(({ filePath, line, message }) => `${filePath}:${line} ${JSON.stringify(message)}`)
+
+ expect(missing).toEqual([])
+ })
+
+ it('uses empty translations only for English plural suffix fragments', () => {
+ const empty = Object.entries(adminLiteralZhCN)
+ .filter(([, translation]) => translation.length === 0)
+ .map(([message]) => message)
+
+ expect(empty).toEqual(['ies', 'y'])
+ })
+
+ it('localizes JSX, accessible attributes, templates, and static configuration', () => {
+ const source = `
+ export function Example({ count }: { count: number }) {
+ const command = { title: 'Dashboard' }
+ return
+ }
+ `
+ const result = transformAdminMessages(source, '/repo/src/admin/Example.tsx', {
+ Dashboard: '仪表盘',
+ 'Open Dashboard': '打开仪表盘',
+ '{0} items': '{0} 项',
+ })
+
+ expect(result).not.toBeNull()
+ expect(result?.code).toContain('__instaticAdminLocalize("Dashboard", "仪表盘")')
+ expect(result?.code).toContain('__instaticAdminLocalize("Open Dashboard", "打开仪表盘")')
+ expect(result?.code).toContain('__instaticAdminFormat("{0} items", "{0} 项", [count])')
+ expect(result?.code).toContain('get title() { return __instaticAdminLocalize')
+ })
+})
diff --git a/src/admin/AppLoadingScreen.tsx b/src/admin/AppLoadingScreen.tsx
index d724d68de..2ab54b9fb 100644
--- a/src/admin/AppLoadingScreen.tsx
+++ b/src/admin/AppLoadingScreen.tsx
@@ -1,12 +1,15 @@
import styles from './AppLoadingScreen.module.css'
+import { useI18n } from './i18n'
export function AppLoadingScreen() {
+ const { t } = useI18n()
+
return (
diff --git a/src/admin/i18n/I18nProvider.tsx b/src/admin/i18n/I18nProvider.tsx
new file mode 100644
index 000000000..e2617d3d9
--- /dev/null
+++ b/src/admin/i18n/I18nProvider.tsx
@@ -0,0 +1,58 @@
+import {
+ Fragment,
+ useEffect,
+ useLayoutEffect,
+ useState,
+ type ReactNode,
+} from 'react'
+import { translate, type AdminLocale, type MessageKey, type MessageParams } from './catalog'
+import {
+ ADMIN_LOCALE_STORAGE_KEY,
+ readAdminLocalePreference,
+ resolveInitialAdminLocale,
+ writeAdminLocalePreference,
+} from './localePreference'
+import { setActiveAdminLocale } from './runtime'
+import { I18nContext } from './context'
+
+interface I18nProviderProps {
+ children: ReactNode
+ initialLocale?: AdminLocale
+}
+
+export function I18nProvider({ children, initialLocale }: I18nProviderProps) {
+ const [locale, setLocaleState] = useState(
+ () => initialLocale ?? resolveInitialAdminLocale(),
+ )
+ setActiveAdminLocale(locale)
+
+ useLayoutEffect(() => {
+ document.documentElement.lang = locale
+ }, [locale])
+
+ useEffect(() => {
+ function handleStorage(event: StorageEvent): void {
+ if (event.key !== ADMIN_LOCALE_STORAGE_KEY) return
+ const storedLocale = readAdminLocalePreference()
+ if (storedLocale) setLocaleState(storedLocale)
+ }
+
+ window.addEventListener('storage', handleStorage)
+ return () => window.removeEventListener('storage', handleStorage)
+ }, [])
+
+ function setLocale(nextLocale: AdminLocale): void {
+ setLocaleState(nextLocale)
+ writeAdminLocalePreference(nextLocale)
+ }
+
+ function t(key: MessageKey, params?: MessageParams): string {
+ return translate(locale, key, params)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/admin/i18n/LanguageSwitcher.tsx b/src/admin/i18n/LanguageSwitcher.tsx
new file mode 100644
index 000000000..49debdbb2
--- /dev/null
+++ b/src/admin/i18n/LanguageSwitcher.tsx
@@ -0,0 +1,25 @@
+import { Button } from '@ui/components/Button'
+import { LOCALE_NATIVE_NAMES } from './catalog'
+import { useI18n } from './context'
+
+interface LanguageSwitcherProps {
+ className?: string
+}
+
+export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
+ const { locale, setLocale, t } = useI18n()
+ const nextLocale = locale === 'en' ? 'zh-CN' : 'en'
+ const nextLocaleName = LOCALE_NATIVE_NAMES[nextLocale]
+
+ return (
+
+ )
+}
diff --git a/src/admin/i18n/catalog.ts b/src/admin/i18n/catalog.ts
new file mode 100644
index 000000000..80f120e76
--- /dev/null
+++ b/src/admin/i18n/catalog.ts
@@ -0,0 +1,80 @@
+export const SUPPORTED_LOCALES = ['en', 'zh-CN'] as const
+
+export type AdminLocale = (typeof SUPPORTED_LOCALES)[number]
+
+export const DEFAULT_ADMIN_LOCALE: AdminLocale = 'zh-CN'
+
+const en = {
+ 'app.loading': 'Loading Instatic',
+ 'language.switchTo': 'Switch language to {language}',
+ 'preauth.setup.title': 'Set Up CMS',
+ 'preauth.login.title': 'Admin Login',
+ 'preauth.mfa.title': 'Two-Factor Authentication',
+ 'preauth.setup.submit': 'Create Admin',
+ 'preauth.setup.submitPending': 'Setting up',
+ 'preauth.login.submit': 'Sign In',
+ 'preauth.login.submitPending': 'Signing in',
+ 'preauth.mfa.submit': 'Verify',
+ 'preauth.mfa.submitPending': 'Verifying',
+ 'preauth.field.authenticationCode': 'Authentication code',
+ 'preauth.field.siteName': 'Site name',
+ 'preauth.field.displayName': 'Your name',
+ 'preauth.field.displayNameHint': 'optional, shown on published pages',
+ 'preauth.field.email': 'Email',
+ 'preauth.field.password': 'Password',
+ 'preauth.setup.defaultSiteName': 'My Site',
+ 'preauth.error.passwordTooShort': 'Password must be at least {min} characters',
+ 'preauth.error.setupFailed': 'Setup failed',
+ 'preauth.error.loginFailed': 'Login failed',
+ 'preauth.error.mfaVerificationFailed': 'MFA verification failed',
+} as const
+
+export type MessageKey = keyof typeof en
+export type MessageParams = Record
+export type TranslationCatalog = Record
+
+const zhCN: TranslationCatalog = {
+ 'app.loading': '正在加载 Instatic',
+ 'language.switchTo': '切换语言为{language}',
+ 'preauth.setup.title': '初始化 CMS',
+ 'preauth.login.title': '管理员登录',
+ 'preauth.mfa.title': '双重身份验证',
+ 'preauth.setup.submit': '创建管理员',
+ 'preauth.setup.submitPending': '正在初始化',
+ 'preauth.login.submit': '登录',
+ 'preauth.login.submitPending': '正在登录',
+ 'preauth.mfa.submit': '验证',
+ 'preauth.mfa.submitPending': '正在验证',
+ 'preauth.field.authenticationCode': '验证码',
+ 'preauth.field.siteName': '站点名称',
+ 'preauth.field.displayName': '你的名字',
+ 'preauth.field.displayNameHint': '可选,将显示在已发布的页面上',
+ 'preauth.field.email': '邮箱',
+ 'preauth.field.password': '密码',
+ 'preauth.setup.defaultSiteName': '我的站点',
+ 'preauth.error.passwordTooShort': '密码至少需要 {min} 个字符',
+ 'preauth.error.setupFailed': '初始化失败',
+ 'preauth.error.loginFailed': '登录失败',
+ 'preauth.error.mfaVerificationFailed': '双重身份验证失败',
+}
+
+const CATALOGS: Record = {
+ en,
+ 'zh-CN': zhCN,
+}
+
+export const LOCALE_NATIVE_NAMES: Record = {
+ en: 'English',
+ 'zh-CN': '简体中文',
+}
+
+export function translate(
+ locale: AdminLocale,
+ key: MessageKey,
+ params: MessageParams = {},
+): string {
+ return CATALOGS[locale][key].replace(/\{([a-zA-Z0-9_]+)\}/g, (placeholder, name: string) => {
+ const value = params[name]
+ return value === undefined ? placeholder : String(value)
+ })
+}
diff --git a/src/admin/i18n/constants.ts b/src/admin/i18n/constants.ts
new file mode 100644
index 000000000..c013eb6ca
--- /dev/null
+++ b/src/admin/i18n/constants.ts
@@ -0,0 +1 @@
+export const ADMIN_LOCALE_STORAGE_KEY = 'instatic-admin-locale-v1'
diff --git a/src/admin/i18n/context.ts b/src/admin/i18n/context.ts
new file mode 100644
index 000000000..0e3b54292
--- /dev/null
+++ b/src/admin/i18n/context.ts
@@ -0,0 +1,18 @@
+import { createContext, useContext } from 'react'
+import { translate, type AdminLocale, type MessageKey, type MessageParams } from './catalog'
+
+export interface I18nContextValue {
+ locale: AdminLocale
+ setLocale: (locale: AdminLocale) => void
+ t: (key: MessageKey, params?: MessageParams) => string
+}
+
+export const I18nContext = createContext({
+ locale: 'en',
+ setLocale: () => {},
+ t: (key, params) => translate('en', key, params),
+})
+
+export function useI18n(): I18nContextValue {
+ return useContext(I18nContext)
+}
diff --git a/src/admin/i18n/index.ts b/src/admin/i18n/index.ts
new file mode 100644
index 000000000..dda0c34fa
--- /dev/null
+++ b/src/admin/i18n/index.ts
@@ -0,0 +1,16 @@
+export { I18nProvider } from './I18nProvider'
+export { useI18n } from './context'
+export { LanguageSwitcher } from './LanguageSwitcher'
+export {
+ DEFAULT_ADMIN_LOCALE,
+ LOCALE_NATIVE_NAMES,
+ SUPPORTED_LOCALES,
+ translate,
+} from './catalog'
+export type { AdminLocale, MessageKey, MessageParams } from './catalog'
+export {
+ ADMIN_LOCALE_STORAGE_KEY,
+ readAdminLocalePreference,
+ resolveInitialAdminLocale,
+ writeAdminLocalePreference,
+} from './localePreference'
diff --git a/src/admin/i18n/literalCatalog.ts b/src/admin/i18n/literalCatalog.ts
new file mode 100644
index 000000000..c315942b0
--- /dev/null
+++ b/src/admin/i18n/literalCatalog.ts
@@ -0,0 +1,33 @@
+import { accountZhCN } from './locales/zh-CN/account'
+import { aiZhCN } from './locales/zh-CN/ai'
+import { contentZhCN } from './locales/zh-CN/content'
+import { dataZhCN } from './locales/zh-CN/data'
+import { mediaZhCN } from './locales/zh-CN/media'
+import { modalsZhCN } from './locales/zh-CN/modals'
+import { dashboardZhCN } from './locales/zh-CN/dashboard'
+import { pluginsZhCN } from './locales/zh-CN/plugins'
+import { sharedZhCN } from './locales/zh-CN/shared'
+import { spotlightZhCN } from './locales/zh-CN/spotlight'
+import { siteAZhCN } from './locales/zh-CN/site-a'
+import { siteBZhCN } from './locales/zh-CN/site-b'
+import { siteCZhCN } from './locales/zh-CN/site-c'
+import { siteDZhCN } from './locales/zh-CN/site-d'
+import { usersZhCN } from './locales/zh-CN/users'
+
+export const adminLiteralZhCN = {
+ ...sharedZhCN,
+ ...dashboardZhCN,
+ ...pluginsZhCN,
+ ...usersZhCN,
+ ...accountZhCN,
+ ...aiZhCN,
+ ...contentZhCN,
+ ...dataZhCN,
+ ...mediaZhCN,
+ ...modalsZhCN,
+ ...spotlightZhCN,
+ ...siteAZhCN,
+ ...siteBZhCN,
+ ...siteCZhCN,
+ ...siteDZhCN,
+} as const satisfies Record
diff --git a/src/admin/i18n/localePreference.ts b/src/admin/i18n/localePreference.ts
new file mode 100644
index 000000000..fda7fe0ab
--- /dev/null
+++ b/src/admin/i18n/localePreference.ts
@@ -0,0 +1,48 @@
+import { Type, type Static } from '@sinclair/typebox'
+import { safeParseJson } from '@core/utils/jsonValidate'
+import { DEFAULT_ADMIN_LOCALE, type AdminLocale } from './catalog'
+import { ADMIN_LOCALE_STORAGE_KEY } from './constants'
+
+export { ADMIN_LOCALE_STORAGE_KEY } from './constants'
+
+const AdminLocalePreferenceSchema = Type.Object(
+ {
+ locale: Type.Union([Type.Literal('en'), Type.Literal('zh-CN')]),
+ },
+ { additionalProperties: true },
+)
+
+type AdminLocalePreference = Static
+
+function browserStorage(): Storage | null {
+ return typeof localStorage === 'undefined' ? null : localStorage
+}
+
+export function readAdminLocalePreference(): AdminLocale | null {
+ const storage = browserStorage()
+ if (!storage) return null
+
+ const raw = storage.getItem(ADMIN_LOCALE_STORAGE_KEY)
+ if (!raw) return null
+
+ const result = safeParseJson(raw, AdminLocalePreferenceSchema)
+ return result.ok ? result.value.locale : null
+}
+
+export function writeAdminLocalePreference(locale: AdminLocale): void {
+ const storage = browserStorage()
+ if (!storage) return
+
+ const preference: AdminLocalePreference = { locale }
+ try {
+ storage.setItem(ADMIN_LOCALE_STORAGE_KEY, JSON.stringify(preference))
+ } catch (_err) {
+ // Locale persistence is best-effort; the live selection still works.
+ }
+}
+
+export function resolveInitialAdminLocale(): AdminLocale {
+ const stored = readAdminLocalePreference()
+ if (stored) return stored
+ return DEFAULT_ADMIN_LOCALE
+}
diff --git a/src/admin/i18n/locales/zh-CN/account.ts b/src/admin/i18n/locales/zh-CN/account.ts
new file mode 100644
index 000000000..c3f0e5601
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/account.ts
@@ -0,0 +1,106 @@
+export const accountZhCN = {
+ "{0} recovery {1} remaining.": "还剩 {0} 个恢复{1}。",
+ "Account sections": "账户页面分区",
+ "Disabling...": "正在关闭…",
+ "Enable MFA": "启用 MFA",
+ "Enable two-factor authentication before generating recovery codes.": "请先启用双重身份验证,再生成恢复码。",
+ "Enabling...": "正在启用…",
+ "Generate codes": "生成恢复码",
+ "Generating...": "正在生成…",
+ "Last changed: {0}": "上次更改:{0}",
+ "Last login: {0}": "上次登录:{0}",
+ "Loading activity": "正在加载活动记录",
+ "Loading sessions": "正在加载会话",
+ "OAuth and passkeys are a separate sign-in provider pass.": "OAuth 和通行密钥属于独立登录提供商流程。",
+ "Off - sensitive actions use the active session only.": "关闭——敏感操作仅使用当前会话。",
+ "On - sensitive actions ask again after {0} minutes.": "开启——{0} 分钟后执行敏感操作会再次验证。",
+ "On{0}": "开启{0}",
+ "Password has not been used yet.": "此密码尚未使用。",
+ "Require step-up authentication": "要求二次验证",
+ "Save password": "保存密码",
+ "Saving...": "正在保存…",
+ "Starting...": "正在启动…",
+ "Step-up window": "二次验证有效期",
+ "15 minutes": "15 分钟",
+ "30 minutes": "30 分钟",
+ "5 minutes": "5 分钟",
+ "60 minutes": "60 分钟",
+ "Account": "账户",
+ "Actions": "操作",
+ "Active devices": "活跃设备",
+ "Active sessions": "活跃会话",
+ "Activity {0}": "活动 {0}",
+ "Authentication code": "验证码",
+ "Change password": "修改密码",
+ "Change picture": "更换头像",
+ "Change your password. Other devices are signed out after a successful update.": "修改密码。更新成功后,其他设备将退出登录。",
+ "Clipboard is not available in this browser.": "此浏览器无法使用剪贴板。",
+ "Confirm new password": "确认新密码",
+ "Connected sign-ins": "关联登录方式",
+ "Copied": "已复制",
+ "Copy key": "复制密钥",
+ "Could not copy the setup key.": "无法复制设置密钥。",
+ "Could not render the QR code. Use the setup key instead.": "无法生成二维码,请改用设置密钥。",
+ "Could not sign out device": "无法退出该设备",
+ "Could not sign out other devices": "无法退出其他设备",
+ "Could not update password.": "无法更新密码。",
+ "Current": "当前",
+ "Device": "设备",
+ "Devices currently signed in to your account. Sign any out individually or all at once. For a record of past sign-in attempts (including failures), see Sign-in history.": "当前登录此账户的设备。你可以单独退出某个设备,也可以一次退出全部设备。过往登录尝试(包括失败记录)请查看“登录历史”。",
+ "Email": "邮箱",
+ "Enable two-factor authentication": "启用双重身份验证",
+ "failed in last 24h": "过去 24 小时内失败",
+ "IP": "IP",
+ "JPEG, PNG, GIF, or WebP, 5 MB maximum.": "支持 JPEG、PNG、GIF 或 WebP,最大 5 MB。",
+ "Last active": "最近活动",
+ "Login activity": "登录活动",
+ "Manage your profile, devices, security, and sign-in activity.": "管理个人资料、设备、安全设置和登录活动。",
+ "Manual setup key": "手动设置密钥",
+ "New password": "新密码",
+ "No active sessions.": "没有活跃会话。",
+ "No login activity yet.": "暂无登录活动。",
+ "No other devices were signed in.": "没有其他设备处于登录状态。",
+ "OAuth providers and passkeys you can use alongside your password.": "可与密码一起使用的 OAuth 提供商和通行密钥。",
+ "One-time codes you can use if you lose access to your authenticator app.": "无法使用验证器应用时可使用的一次性代码。",
+ "Open authenticator app": "打开验证器应用",
+ "Outcome": "结果",
+ "Password updated. Other devices were signed out.": "密码已更新,其他设备已退出登录。",
+ "Password, step-up authentication, two-factor authentication, and connected sign-ins.": "密码、二次验证、双重身份验证和关联登录方式。",
+ "Profile": "个人资料",
+ "QR code unavailable": "二维码不可用",
+ "Recent sign-in attempts on your account, including failures and lockouts. To revoke a current session, use Active devices.": "此账户近期的登录尝试,包括失败和锁定记录。要撤销当前会话,请前往“活跃设备”。",
+ "Recovery codes": "恢复码",
+ "Recovery codes regenerated.": "恢复码已重新生成。",
+ "Removing…": "正在移除…",
+ "Rendering QR code": "正在生成二维码",
+ "Require password confirmation again before sensitive account and publishing actions.": "执行敏感账户操作或发布操作前,要求再次确认密码。",
+ "Role": "角色",
+ "Save profile": "保存个人资料",
+ "Save these recovery codes now. They will not be shown again.": "请立即保存这些恢复码,它们不会再次显示。",
+ "Saving…": "正在保存…",
+ "Scan the QR code": "扫描二维码",
+ "Scan this QR code with your authenticator app": "使用验证器应用扫描此二维码",
+ "Security": "安全",
+ "Session {0}": "会话 {0}",
+ "Sign out everywhere else": "退出其他所有设备",
+ "Sign-in history": "登录历史",
+ "Signed in": "已登录",
+ "Signed out {0} other {1}.": "已退出另外 {0} 个{1}。",
+ "Signed out {0}.": "已退出 {0}。",
+ "Step-up authentication": "二次验证",
+ "Step-up authentication disabled.": "二次验证已关闭。",
+ "Step-up authentication updated to {0} minutes.": "二次验证有效期已更新为 {0} 分钟。",
+ "Suspicious activity in the last 24 hours. Review the entries below — if any are unfamiliar, consider changing your password once that lands.": "过去 24 小时内存在可疑活动。请检查下方记录;如发现陌生活动,建议修改密码。",
+ "This device": "当前设备",
+ "Two-factor authentication": "双重身份验证",
+ "Two-factor authentication disabled.": "双重身份验证已关闭。",
+ "Two-factor authentication enabled.": "双重身份验证已启用。",
+ "unknown": "未知",
+ "Unknown device": "未知设备",
+ "Upload picture": "上传头像",
+ "Uploading…": "正在上传…",
+ "Use a TOTP authenticator app as a second factor when signing in.": "登录时使用 TOTP 验证器应用作为第二重验证。",
+ "Use Google Authenticator, 1Password, Microsoft Authenticator, Authy, or any TOTP app.": "可使用 Google Authenticator、1Password、Microsoft Authenticator、Authy 或任意 TOTP 应用。",
+ "When": "时间",
+ "Your name, email, and role across the install.": "此安装中的姓名、邮箱和角色。",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/ai.ts b/src/admin/i18n/locales/zh-CN/ai.ts
new file mode 100644
index 000000000..f1b190d54
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/ai.ts
@@ -0,0 +1,207 @@
+export const aiZhCN = {
+ "Audit range": "审计范围",
+ "Configured": "已配置",
+ "Connected": "已连接",
+ "Creating…": "正在创建…",
+ "Model for {0}": "{0}的模型",
+ "Re-enter key": "重新输入密钥",
+ "Set defaults": "设置默认值",
+ ", then completely restart Claude Desktop. The bridge requires Node 18 or newer.": ",然后完全重启 Claude Desktop。此桥接需要 Node 18 或更高版本。",
+ "{0} connected": "已连接 {0} 个",
+ "{0} models": "{0} 个模型",
+ "{0} permissions": "{0} 项权限",
+ "{0} scopes": "{0} 个作用域",
+ "{0} users": "{0} 位用户",
+ "1 year": "1 年",
+ "30 days": "30 天",
+ "7 days": "7 天",
+ "90 days": "90 天",
+ "Access token created": "访问令牌已创建",
+ "Add connection": "添加连接",
+ "Add provider": "添加提供商",
+ "After approval, Instatic sends a one-time authorization code to this registered callback:": "批准后,Instatic 会将一次性授权码发送到此已注册的回调地址:",
+ "AI calls made through the plugin API.": "通过插件 API 发起的 AI 调用。",
+ "AI settings": "AI 设置",
+ "AI workspace": "AI 工作区",
+ "All time": "全部时间",
+ "Allow writes": "允许写入",
+ "and clears access for any AI surface that depends on it.": ",并撤销所有依赖此凭据的 AI 功能访问权限。",
+ "Anthropic": "Anthropic",
+ "API key": "API 密钥",
+ "Assistant text + tool-call envelopes.": "助手文本与工具调用封装。",
+ "Authentication": "身份验证",
+ "Authorize connection": "授权连接",
+ "Authorize MCP connection": "授权 MCP 连接",
+ "Authorized": "已授权",
+ "Authorized connection": "已授权连接",
+ "Authorizing…": "正在授权…",
+ "Available": "可用",
+ "Available models": "可用模型",
+ "Available on this device only": "仅在此设备可用",
+ "Back to AI settings": "返回 AI 设置",
+ "Base URL": "基础 URL",
+ "Bearer token": "Bearer 令牌",
+ "Best-effort estimate from the price table.": "根据价格表估算,仅供参考。",
+ "By model": "按模型",
+ "By surface": "按功能界面",
+ "Cache hit": "缓存命中",
+ "Cached reads ÷ total input. Higher = bigger cost savings.": "缓存读取量 ÷ 总输入量。数值越高,节省的费用越多。",
+ "capabilities approved for this connection.": "项已批准的连接权限。",
+ "Chats": "对话",
+ "Choose allowed capabilities": "选择允许的权限",
+ "Choose Connect, then approve the requested capabilities in Instatic.": "选择“连接”,然后在 Instatic 中批准所请求的权限。",
+ "Choose credential and model": "选择凭据和模型",
+ "Choose only the permissions and lifetime this client needs.": "仅选择此客户端所需的权限和有效期。",
+ "Claude Code and compatible CLIs": "Claude Code 和兼容的 CLI",
+ "Claude Desktop with local Instatic": "Claude Desktop 与本地 Instatic",
+ "Claude models with strong tool use and long-context reasoning.": "擅长工具调用和长上下文推理的 Claude 模型。",
+ "claude_desktop_config.json": "claude_desktop_config.json",
+ "Claude, ChatGPT, and remote agents authorize through Instatic.": "Claude、ChatGPT 和远程智能体通过 Instatic 授权。",
+ "Client names are self-declared. Approve only if you started this connection and recognize the callback address above.": "客户端名称由其自行声明。仅在你发起了此连接且确认上方回调地址时批准。",
+ "Command": "命令",
+ "Connect": "连接",
+ "Connect {0}": "连接{0}",
+ "Connect a provider first": "请先连接提供商",
+ "Connect a remote client": "连接远程客户端",
+ "Connect any OpenAI-compatible API endpoint.": "连接任意 OpenAI 兼容 API 端点。",
+ "Connect Claude Code, Codex, Cursor, Desktop, or another local client.": "连接 Claude Code、Codex、Cursor、Desktop 或其他本地客户端。",
+ "Connect from the client": "从客户端连接",
+ "Connecting…": "正在连接…",
+ "Connection": "连接",
+ "Connection details": "连接详情",
+ "Connection is healthy. {0} models available.": "连接正常,有 {0} 个可用模型。",
+ "Connection request unavailable": "连接请求不可用",
+ "Connection test failed": "连接测试失败",
+ "Connection test failed.": "连接测试失败。",
+ "Connector endpoint": "连接器端点",
+ "Copy this token now. Instatic stores only its hash and cannot show it again.": "请立即复制此令牌。Instatic 只存储其哈希值,之后无法再次显示。",
+ "Could not authorize connection": "无法授权连接",
+ "Could not clear {0} default": "无法清除{0}默认值",
+ "Could not connect {0}": "无法连接{0}",
+ "Could not copy to clipboard": "无法复制到剪贴板",
+ "Could not create access token": "无法创建访问令牌",
+ "Could not deny connection": "无法拒绝连接",
+ "Could not disconnect client": "无法断开客户端",
+ "Could not remove credential": "无法移除凭据",
+ "Could not revoke token": "无法撤销令牌",
+ "Could not save {0} default": "无法保存{0}默认值",
+ "Create a personal token": "创建个人令牌",
+ "Create access token": "创建访问令牌",
+ "Create personal access token": "创建个人访问令牌",
+ "Created": "创建时间",
+ "Credential": "凭据",
+ "Credential and model": "凭据和模型",
+ "Credential removed": "凭据已移除",
+ "Credentials": "凭据",
+ "Current:": "当前:",
+ "Custom endpoint": "自定义端点",
+ "Daily spend": "每日费用",
+ "Data workspace assistance and table operations.": "数据工作区辅助和数据表操作。",
+ "days": "天",
+ "days and can be disconnected at any time.": "天,并可随时断开连接。",
+ "Default model": "默认模型",
+ "Default model settings": "默认模型设置",
+ "Defaults": "默认值",
+ "Deny": "拒绝",
+ "Desktop configuration": "桌面端配置",
+ "Disconnect client": "断开客户端",
+ "Display label": "显示名称",
+ "Distinct conversations with activity.": "有活动记录的不同对话数。",
+ "e.g. {0} production": "例如:{0} 生产环境",
+ "e.g. MacBook — Claude Desktop": "例如:MacBook — Claude Desktop",
+ "Each client receives its own independently revocable credential.": "每个客户端都会获得可单独撤销的凭据。",
+ "Endpoint": "端点",
+ "Expires": "到期时间",
+ "Expires after": "有效期",
+ "General-purpose language and multimodal models from OpenAI.": "OpenAI 提供的通用语言和多模态模型。",
+ "Generated setup commands include the correct MCP endpoint and authorization header.": "生成的设置命令包含正确的 MCP 端点和授权请求头。",
+ "Go to Providers": "前往提供商",
+ "Granted permissions": "已授予权限",
+ "Hosted clients": "托管客户端",
+ "Hosted OAuth": "托管 OAuth",
+ "Hosted OAuth client": "托管 OAuth 客户端",
+ "Input tokens": "输入 Token",
+ "Instatic validates and encrypts the credential before storing it.": "Instatic 会在存储前验证并加密凭据。",
+ "Last used": "上次使用",
+ "Leave blank if no auth": "无需验证时留空",
+ "Leave blank when the endpoint does not require authentication.": "端点不需要身份验证时请留空。",
+ "Lifecycle and authentication information for this client.": "此客户端的生命周期和身份验证信息。",
+ "Loading authorization request…": "正在加载授权请求…",
+ "Loading connections…": "正在加载连接…",
+ "Loading defaults…": "正在加载默认值…",
+ "Loading model catalogue…": "正在加载模型目录…",
+ "Loading…": "正在加载…",
+ "Local and CLI": "本地与 CLI",
+ "Local and CLI clients": "本地与 CLI 客户端",
+ "MCP connection browser": "MCP 连接浏览器",
+ "MCP connections": "MCP 连接",
+ "Merge this entry into": "将此项合并到",
+ "Model routing": "模型路由",
+ "Model routing becomes available after at least one credential is ready.": "至少有一个凭据就绪后,模型路由才可用。",
+ "Models returned by this provider for the current credential.": "此提供商为当前凭据返回的模型。",
+ "Models, defaults, connections, and usage.": "模型、默认值、连接和用量。",
+ "Needs attention": "需要处理",
+ "No AI activity in this range yet.": "此时间范围内暂无 AI 活动。",
+ "No daily activity in this range.": "此时间范围内暂无每日活动。",
+ "No expiry": "永不过期",
+ "No model activity yet.": "暂无模型活动。",
+ "No models reported by this provider.": "此提供商未返回模型。",
+ "No surface activity yet.": "暂无功能界面活动。",
+ "Not configured": "未配置",
+ "OAuth Client ID and Secret are discovered automatically.": "OAuth 客户端 ID 和密钥会自动发现。",
+ "Ollama": "Ollama",
+ "Open the client's connector settings and choose a custom MCP connector.": "打开客户端的连接器设置,并选择自定义 MCP 连接器。",
+ "OpenAI": "OpenAI",
+ "OpenRouter": "OpenRouter",
+ "Optional": "可选",
+ "Output tokens": "输出 Token",
+ "Paste the URL above. Leave OAuth Client ID and Secret empty.": "粘贴上方 URL,并将 OAuth 客户端 ID 和密钥留空。",
+ "Per-user and per-surface AI usage with token + cost rollups.": "按用户和功能界面汇总 AI Token 用量与费用。",
+ "Permissions": "权限",
+ "Personal access token": "个人访问令牌",
+ "Personal token": "个人令牌",
+ "Prompt + cached input combined.": "提示词与缓存输入的总和。",
+ "Provider browser": "提供商浏览器",
+ "Providers": "提供商",
+ "Public HTTPS is ready": "公开 HTTPS 已就绪",
+ "Read": "读取",
+ "Read access is selected by default. Writes and publishing stay off until you explicitly enable them.": "默认选择读取权限。写入和发布权限只有在你明确启用后才会开启。",
+ "Remote MCP URL": "远程 MCP URL",
+ "Remote OAuth": "远程 OAuth",
+ "Remove credential": "移除凭据",
+ "Remove credential?": "移除凭据?",
+ "Requesting client": "请求连接的客户端",
+ "Review the client and choose exactly what it may do in this Instatic instance.": "检查客户端,并准确选择它在此 Instatic 实例中可执行的操作。",
+ "Revoke token": "撤销令牌",
+ "Revoked": "已撤销",
+ "Route requests to models from multiple providers through one API.": "通过一个 API 将请求路由到多个提供商的模型。",
+ "Run local models on infrastructure you control.": "在你控制的基础设施上运行本地模型。",
+ "Run this command in your terminal.": "在终端中运行此命令。",
+ "Save default": "保存默认值",
+ "Scoped access": "限定作用域的访问",
+ "Site editing": "站点编辑",
+ "Site editor": "站点编辑器",
+ "Spend": "费用",
+ "Stored encrypted and never displayed again.": "加密存储,之后不会再次显示。",
+ "Test connection": "测试连接",
+ "The client returns here so you can choose permissions and approve access.": "客户端会返回此处,以便你选择权限并批准访问。",
+ "The connection expires after": "连接将在以下时间后到期:",
+ "The provider rejected the credential.": "提供商拒绝了此凭据。",
+ "The root URL of the compatible API.": "兼容 API 的根 URL。",
+ "The saved credential is no longer available. Choose a replacement.": "已保存的凭据不再可用,请选择替代凭据。",
+ "The secret is displayed once and stored only as a hash.": "密钥只显示一次,并且仅以哈希形式存储。",
+ "This immediately prevents the client from calling Instatic tools.": "这会立即阻止客户端调用 Instatic 工具。",
+ "This permanently deletes the credential and removes access for all scopes.": "这会永久删除凭据,并撤销所有作用域的访问权限。",
+ "This permanently removes": "这会永久移除",
+ "Today": "今天",
+ "Token": "令牌",
+ "Token name": "令牌名称",
+ "Top users by cost": "按费用排名的用户",
+ "Uncached billed input. {0} more served from cache.": "未缓存的计费输入;另有 {0} 来自缓存。",
+ "Usage audit": "用量审计",
+ "Use a clear device or client name so you can revoke this credential without guessing later.": "使用清晰的设备或客户端名称,方便日后准确撤销此凭据。",
+ "Use a name teammates will recognize in model pickers.": "使用团队成员能在模型选择器中识别的名称。",
+ "Users can still choose another available model for an individual conversation.": "用户仍可为单次对话选择其他可用模型。",
+ "Visual editor chat and page-building tools.": "可视化编辑器对话和页面构建工具。",
+ "Writing, editing, and structured content workflows.": "写作、编辑和结构化内容工作流。",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/content.ts b/src/admin/i18n/locales/zh-CN/content.ts
new file mode 100644
index 000000000..507d90a88
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/content.ts
@@ -0,0 +1,142 @@
+export const contentZhCN = {
+ "Content Explorer": "内容资源管理器",
+ "Content item options": "内容项目选项",
+ "Draft saved": "草稿已保存",
+ "Entries": "条目",
+ "entry": "条目",
+ "Insert block": "插入区块",
+ "Live mode (edit inside the rendered template)": "线上模式(在已渲染模板中编辑)",
+ "New {0}": "新建{0}",
+ "No entry selected": "未选择条目",
+ "Rename collection": "重命名内容集合",
+ "Resize content sidebar": "调整内容侧边栏大小",
+ "Retry publish": "重试发布",
+ "Save failed": "保存失败",
+ "Turn into": "转换为",
+ "Unknown author": "未知作者",
+ "Unknown user": "未知用户",
+ "Write mode (plain editor surface)": "写作模式(纯编辑器界面)",
+ "{0} {1} panel": "{0}{1}面板",
+ "{0} upload preview": "{0}上传预览",
+ "2-column, 3-row": "2 列 3 行",
+ "AI assistant": "AI 助手",
+ "Align center": "居中对齐",
+ "Align left": "左对齐",
+ "Align right": "右对齐",
+ "Alt": "替代文本",
+ "Alt text": "替代文本",
+ "Apply": "应用",
+ "Author": "作者",
+ "Block options": "区块选项",
+ "Block quote": "块引用",
+ "Body": "正文",
+ "Bold (Cmd-B)": "粗体(Cmd-B)",
+ "Bullet list": "项目符号列表",
+ "Cancel upload": "取消上传",
+ "Clone this block below": "在下方复制此区块",
+ "Code block": "代码块",
+ "Collection": "内容集合",
+ "Collection settings": "内容集合设置",
+ "Collections": "内容集合",
+ "Content author": "内容作者",
+ "Content canvas": "内容画布",
+ "Content editor mode": "内容编辑器模式",
+ "Content panel dock": "内容面板停靠区",
+ "Content settings": "内容设置",
+ "Convert to draft": "转为草稿",
+ "Copy URL": "复制 URL",
+ "Could not create collection": "无法创建内容集合",
+ "Could not load media": "无法加载媒体",
+ "Could not update collection": "无法更新内容集合",
+ "Create the first": "创建第一条",
+ "Data token": "数据 Token",
+ "Describe the image…": "描述这张图片…",
+ "Divider": "分隔线",
+ "Draft": "草稿",
+ "Duplicate": "复制",
+ "Edit alt text": "编辑替代文本",
+ "Entry": "条目",
+ "Featured media": "特色媒体",
+ "Fenced code": "围栏代码块",
+ "Fields": "字段",
+ "From media library": "从媒体库选择",
+ "Heading": "标题",
+ "Heading 2": "二级标题",
+ "Heading 3": "三级标题",
+ "Heading 4": "四级标题",
+ "Horizontal rule": "水平分隔线",
+ "Image": "图片",
+ "Image / Video": "图片/视频",
+ "Image or video from library": "媒体库中的图片或视频",
+ "Inline code": "行内代码",
+ "Insert {source.field}": "插入 {source.field}",
+ "Insert block below": "在下方插入区块",
+ "Insert data token": "插入数据 Token",
+ "Italic (Cmd-I)": "斜体(Cmd-I)",
+ "Justify": "两端对齐",
+ "Link (Cmd-K)": "链接(Cmd-K)",
+ "Link URL": "链接 URL",
+ "Live": "线上",
+ "Live mode needs at least one published version of the site so it can resolve the entry template. Publish the site once and try again.": "线上模式需要站点至少发布过一个版本,才能解析条目模板。请先发布一次站点后重试。",
+ "Live preview": "线上预览",
+ "Live preview unavailable": "线上预览不可用",
+ "Loading content": "正在加载内容",
+ "Loading content settings": "正在加载内容设置",
+ "Loading entries": "正在加载条目",
+ "Move to collection": "移动到内容集合",
+ "New collection": "新建内容集合",
+ "No entries yet.": "暂无条目。",
+ "Not available": "不可用",
+ "Numbered list": "编号列表",
+ "Open in new tab": "在新标签页打开",
+ "Open live {0}": "打开线上{0}",
+ "Ordered list": "有序列表",
+ "Paragraph": "段落",
+ "Plain text": "纯文本",
+ "Plural label": "复数名称",
+ "Post body": "文章正文",
+ "Preview failed": "预览失败",
+ "Product": "产品",
+ "products": "产品",
+ "Products": "产品",
+ "Public URL": "公开 URL",
+ "Publish": "发布",
+ "Published": "已发布",
+ "Quote": "引用",
+ "Remove failed upload": "移除失败的上传任务",
+ "Remove this block": "移除此区块",
+ "Replace media": "替换媒体",
+ "Save": "保存",
+ "Save draft": "保存草稿",
+ "Schedule {0}…": "计划发布{0}…",
+ "Scheduled": "已计划",
+ "Section title": "章节标题",
+ "Select a collection and create an entry to start writing.": "选择一个内容集合并创建条目,即可开始写作。",
+ "SEO description": "SEO 描述",
+ "SEO fields": "SEO 字段",
+ "SEO title": "SEO 标题",
+ "Singular label": "单数名称",
+ "Small heading": "小标题",
+ "Strikethrough": "删除线",
+ "Sub-section": "子章节",
+ "Table": "表格",
+ "Text": "文本",
+ "The preview pipeline could not render this entry.": "预览流程无法渲染此条目。",
+ "Tool {0} failed.": "工具 {0} 执行失败。",
+ "Turn into…": "转换为…",
+ "Unable to rename item": "无法重命名项目",
+ "Underline (Cmd-U)": "下划线(Cmd-U)",
+ "Unordered list": "无序列表",
+ "Unpublished": "未发布",
+ "Untitled": "未命名",
+ "Upload failed": "上传失败",
+ "Uploading… {0}%": "正在上传… {0}%",
+ "URL path": "URL 路径",
+ "Use the schedule dialog to set a publish time": "使用计划发布对话框设置发布时间",
+ "Video": "视频",
+ "Write": "写作",
+ "You do not have permission to do that": "你无权执行此操作",
+ "Your role cannot edit this entry": "你的角色无权编辑此条目",
+ "Your role cannot manage content collections": "你的角色无权管理内容集合",
+ "Your role cannot publish this entry": "你的角色无权发布此条目",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/dashboard.ts b/src/admin/i18n/locales/zh-CN/dashboard.ts
new file mode 100644
index 000000000..191f08c8d
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/dashboard.ts
@@ -0,0 +1,167 @@
+export const dashboardZhCN = {
+ "— drag, resize, or add blocks.": "——可拖动、调整大小或添加区块。",
+ "· self-hosted": "· 自托管",
+ "· top: {0}": "· 最高:{0}",
+ "{0}d": "{0} 天",
+ "{0}d ago": "{0} 天前",
+ "{0}h": "{0} 小时",
+ "{0}h ago": "{0} 小时前",
+ "{0}m": "{0} 分钟",
+ "{0}m ago": "{0} 分钟前",
+ "1 update": "1 项更新",
+ "2h ago": "2 小时前",
+ "30d": "30 天",
+ "3m ago": "3 分钟前",
+ "7d": "7 天",
+ "Add members": "添加成员",
+ "Add SEO, comments, image optimization or workflow extensions from the registry.": "从插件库添加 SEO、评论、图像优化或工作流扩展。",
+ "added user": "添加了用户",
+ "AI cost per day this month": "本月每日 AI 费用",
+ "assigned role": "分配了角色",
+ "Browse plugins": "浏览插件",
+ "categor": "分类",
+ "changed password for": "修改了密码:",
+ "changed status of": "更改了状态:",
+ "chats this month": "本月对话",
+ "Completed": "已完成",
+ "created": "创建了",
+ "created collection": "创建了内容集合",
+ "created role": "创建了角色",
+ "Customize dashboard": "自定义仪表盘",
+ "Customize mode": "自定义模式",
+ "deleted": "删除了",
+ "deleted collection": "删除了内容集合",
+ "disabled plugin": "禁用了插件",
+ "Documents": "文档",
+ "edited": "编辑了",
+ "edited collection": "编辑了内容集合",
+ "Editors, designers and developers — each role gets a tuned set of editor permissions.": "邀请编辑、设计师和开发者;每种角色都有针对性的编辑权限。",
+ "enabled plugin": "启用了插件",
+ "error": "错误",
+ "files ·": "个文件 ·",
+ "ies": "",
+ "in {0}d": "{0} 天后",
+ "in {0}h": "{0} 小时后",
+ "in {0}m": "{0} 分钟后",
+ "In progress": "进行中",
+ "installed plugin": "安装了插件",
+ "installed plugin pack": "安装了插件资源包",
+ "just now": "刚刚",
+ "moved": "移动了",
+ "Not started": "未开始",
+ "now": "刚刚",
+ "off": "关闭",
+ "Pick a favicon, logo and site title. Used everywhere — admin chrome, OG tags, published pages.": "选择站点图标、徽标和标题,它们会用于后台界面、OG 标签和已发布页面。",
+ "Postgres": "Postgres",
+ "Publish all": "全部发布",
+ "published": "已发布",
+ "published the site": "发布了站点",
+ "reassigned author of": "重新分配了作者:",
+ "removed plugin": "移除了插件",
+ "removed role": "移除了角色",
+ "removed user": "移除了用户",
+ "soon": "即将",
+ "SQLite": "SQLite",
+ "Start from a blank canvas, a starter layout, or import HTML and we will scaffold a tree.": "从空白画布、起始布局开始,或导入 HTML 来自动生成页面树。",
+ "suspended user": "停用了用户",
+ "this week": "本周",
+ "Time range": "时间范围",
+ "Total ·": "总计 ·",
+ "Total · no categories yet": "总计 · 暂无分类",
+ "unscheduled": "已取消计划",
+ "updated plugin": "更新了插件",
+ "updated role": "更新了角色",
+ "updated settings for": "更新了设置:",
+ "updated user": "更新了用户",
+ "used ·": "已使用 ·",
+ "Variables only, the full utility framework, or skip it and bring your own CSS.": "选择仅变量、完整实用类框架,或跳过并使用自己的 CSS。",
+ "y": "",
+ "Good {0}, {1}.": "{1},{0}好。",
+ "yest.": "昨天",
+ "yesterday": "昨天",
+ "{0} block preview": "{0}区块预览",
+ "{0} of {1} steps complete": "已完成 {0}/{1} 个步骤",
+ "A record": "A 记录",
+ "active": "活跃",
+ "Activity": "活动",
+ "Add": "添加",
+ "Add {0} to dashboard": "将{0}添加到仪表盘",
+ "Add block": "添加区块",
+ "Admin": "管理员",
+ "AI usage": "AI 用量",
+ "Avatar for {0}": "{0} 的头像",
+ "Backup": "备份",
+ "block": "区块",
+ "Block library": "区块库",
+ "Block library — drop zone": "区块库放置区域",
+ "blocks": "区块",
+ "Build": "构建",
+ "Built-in blocks": "内置区块",
+ "Choose Core Framework import": "选择核心框架导入方式",
+ "Close block library": "关闭区块库",
+ "Create your first page": "创建第一个页面",
+ "Customize": "自定义",
+ "Dashboard": "仪表盘",
+ "Disk usage breakdown": "磁盘用量明细",
+ "Dismiss": "忽略",
+ "DNS + SSL status": "DNS 与 SSL 状态",
+ "Domain": "域名",
+ "Done": "完成",
+ "draft": "草稿",
+ "Drag a block from the grid down here to put it back in the library.": "将网格中的区块拖到这里,即可放回区块库。",
+ "Drag a block onto the grid, or click to append it to the bottom.": "将区块拖到网格中,或点击后添加到底部。",
+ "Drop on grid to add": "放到网格中以添加",
+ "Drop to put back in library": "放下以移回区块库",
+ "Every block is on your dashboard.": "所有区块都已添加到仪表盘。",
+ "Files & thumbnails": "文件与缩略图",
+ "Finish setting up your site": "完成站点设置",
+ "HTTPS": "HTTPS",
+ "Install a plugin": "安装插件",
+ "Installed & updates": "已安装与更新",
+ "instatic.com": "instatic.com",
+ "Invite your team": "邀请团队成员",
+ "Library — drop here to remove": "区块库——拖到这里以移除",
+ "Media": "媒体",
+ "No blocks match “": "没有区块匹配“",
+ "No plugins installed yet.": "尚未安装插件。",
+ "Nothing has happened yet — edits, publishes, and plugin changes will appear here.": "暂无活动;编辑、发布和插件变更会显示在这里。",
+ "Nothing in the lineup yet — schedule, publish, or draft a row to see it here.": "发布队列暂无内容;计划、发布或保存草稿后会显示在这里。",
+ "of": "共",
+ "Open {0} in viewer": "在查看器中打开{0}",
+ "Open in viewer": "在查看器中打开",
+ "Overview": "概览",
+ "Pages": "页面",
+ "Plugin · {0}": "插件 · {0}",
+ "Plugin-provided blocks": "插件提供的区块",
+ "Plugins": "插件",
+ "Posts": "文章",
+ "Posts by category": "按分类统计文章",
+ "Publish lineup": "发布队列",
+ "Published + drafts": "已发布与草稿",
+ "Recent edits & publishes": "最近编辑与发布",
+ "Resize {0} from bottom": "从底部调整{0}大小",
+ "Resize {0} from corner": "从角落调整{0}大小",
+ "Resize {0} from left": "从左侧调整{0}大小",
+ "Resize {0} from right": "从右侧调整{0}大小",
+ "Resize {0} from top": "从顶部调整{0}大小",
+ "Resize block library": "调整区块库大小",
+ "s": "秒",
+ "scheduled": "已计划",
+ "Scheduled, recently published, and drafts": "已计划、最近发布和草稿",
+ "Search blocks": "搜索区块",
+ "Search blocks…": "搜索区块…",
+ "Set site identity": "设置站点标识",
+ "Site": "站点",
+ "Spend + chats this month": "本月费用与对话",
+ "SSL · auto-renew": "SSL · 自动续期",
+ "Status": "状态",
+ "STEP": "步骤",
+ "steps complete. Hit each one in any order — your site is live the moment you publish a page.": "个步骤已完成。你可以按任意顺序完成;页面一经发布,站点即可上线。",
+ "Storage": "存储",
+ "System": "系统",
+ "Uptime, builds, backups": "运行时间、构建与备份",
+ "v": "版本",
+ "verified": "已验证",
+ "You don't have permission to see site-wide AI usage.": "你无权查看全站 AI 用量。",
+ "Your site at a glance — content, activity, storage and plugins. Configure the grid to surface exactly what you watch.": "一览站点的内容、活动、存储和插件;可自定义网格,只显示你关心的信息。",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/data.ts b/src/admin/i18n/locales/zh-CN/data.ts
new file mode 100644
index 000000000..1b69555e9
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/data.ts
@@ -0,0 +1,266 @@
+export const dataZhCN = {
+ "— none —": "— 无 —",
+ ". Untick anything you want to leave out.": "。取消勾选不想包含的内容。",
+ "(e.g. USD)": "(例如 USD)",
+ "(optional)": "(可选)",
+ "(untitled)": "(未命名)",
+ "{0} data grid": "{0}数据网格",
+ "{0} for {1}": "{1}的{0}",
+ "{0} is the primary field": "{0} 是主字段",
+ "{0} row{1} published.": "已发布 {0} 行。",
+ "{0} view": "{0}视图",
+ "{0}: Edit {1}": "{0}:编辑{1}",
+ "+ Add": "+ 添加",
+ "+ More": "+ 更多",
+ "Add a text, number, date, select, URL, or email field to label collapsed items.": "添加文本、数字、日期、选择、URL 或邮箱字段,用作折叠项目的名称。",
+ "Add at least one field to describe a repeated item.": "请至少添加一个字段来描述重复项目。",
+ "Add item": "添加项目",
+ "Add item field": "添加项目字段",
+ "Add media": "添加媒体",
+ "Add media…": "添加媒体…",
+ "Add one structured item, then repeat as needed.": "先添加一个结构化项目,再按需重复。",
+ "Add option": "添加选项",
+ "Add row": "添加数据行",
+ "Add the first row to get started.": "添加第一行即可开始。",
+ "All": "全部",
+ "Allow multiple": "允许多选",
+ "Allow multiple media": "允许多个媒体",
+ "Allow multiple relations": "允许多个关联",
+ "Any": "任意",
+ "Archive": "归档",
+ "Archived": "已归档",
+ "Authoring": "编辑体验",
+ "Automatic": "自动",
+ "Boolean": "布尔值",
+ "Build the reusable group of values stored inside every item.": "构建存储在每个项目中的可复用值组。",
+ "built-in": "内置",
+ "Check your connection and try again.": "请检查网络连接后重试。",
+ "Choose a data table from the sidebar to view and edit its rows.": "从侧边栏选择数据表,以查看和编辑数据行。",
+ "Choose…": "选择…",
+ "Clear media": "清除媒体",
+ "Clear relation": "清除关联",
+ "Clear sort": "清除排序",
+ "Configure how this field accepts and presents values.": "配置此字段如何接收和呈现值。",
+ "Controls the value and authoring UI.": "控制字段值和编辑界面。",
+ "Could not add field": "无法添加字段",
+ "Could not add row": "无法添加数据行",
+ "Could not create field": "无法创建字段",
+ "Could not create table": "无法创建数据表",
+ "Could not duplicate row": "无法复制数据行",
+ "Could not load rows": "无法加载数据行",
+ "Could not save": "无法保存",
+ "Could not save field": "无法保存字段",
+ "Could not save row": "无法保存数据行",
+ "Currency": "货币",
+ "Currency code": "货币代码",
+ "Custom tables": "自定义表",
+ "Danger zone": "危险区域",
+ "Data {0}": "数据{0}",
+ "Data canvas": "数据画布",
+ "Data panel dock": "数据面板停靠区",
+ "Data publish failed": "数据发布失败",
+ "Data published": "数据已发布",
+ "Data table": "数据表",
+ "Data tables": "数据表",
+ "Data tables panel": "数据表面板",
+ "Date": "日期",
+ "Date & time": "日期和时间",
+ "Default value": "默认值",
+ "Delete \"{0}\"?": "删除“{0}”?",
+ "Delete field \"{0}\"?": "删除字段“{0}”?",
+ "Delete item": "删除项目",
+ "Delete item {0}": "删除项目 {0}",
+ "Delete row": "删除数据行",
+ "Delete table": "删除数据表",
+ "Delete table \"{0}\"?": "删除数据表“{0}”?",
+ "Deselect all rows": "取消选择所有行",
+ "Deselect row {0}": "取消选择第 {0} 行",
+ "Draft changes": "草稿更改",
+ "Draft save failed": "草稿保存失败",
+ "Drafts": "草稿",
+ "Duplicate item {0}": "复制项目 {0}",
+ "Duplicate row": "复制数据行",
+ "Each repeated item stores this same group of fields. Repeaters cannot be nested.": "每个重复项目都会存储相同的一组字段。重复器不能嵌套。",
+ "Edit": "编辑",
+ "Edit {0}": "编辑{0}",
+ "Edit in Content": "在内容中编辑",
+ "Empty": "空",
+ "Enter a value…": "输入值…",
+ "Entry URL pattern": "条目 URL 规则",
+ "Entry URLs use /{0}/…": "条目 URL 使用 /{0}/…",
+ "Estimated size · ": "预计大小 · ",
+ "Everything is selected for a": "已选择全部内容,用于",
+ "Export": "导出",
+ "Export categories": "导出分类",
+ "Export failed": "导出失败",
+ "Export row": "导出数据行",
+ "Export site": "导出站点",
+ "Export started": "已开始导出",
+ "Field behavior": "字段行为",
+ "Field creation failed": "字段创建失败",
+ "Field identity": "字段标识",
+ "Field update failed": "字段更新失败",
+ "Format": "格式",
+ "full export": "完整导出",
+ "gallery item {0}": "图库项目 {0}",
+ "General": "常规",
+ "Generated from the label. You can change it.": "根据名称自动生成,可以修改。",
+ "Generated from the name; edit to change entry URLs.": "根据名称自动生成;修改后会改变条目 URL。",
+ "Grid view": "网格视图",
+ "ID": "ID",
+ "ID is fixed after creation.": "ID 创建后不可修改。",
+ "Include {0}": "包含{0}",
+ "Include {0} in export": "在导出中包含{0}",
+ "Inspect row": "检查数据行",
+ "Integer only": "仅整数",
+ "item": "项目",
+ "Item": "项目",
+ "Item fields": "项目字段",
+ "Item structure": "项目结构",
+ "Item summary": "项目摘要",
+ "items": "项目",
+ "Label": "名称",
+ "List view": "列表视图",
+ "Loading": "正在加载",
+ "Loading data rows": "正在加载数据行",
+ "Loading data tables": "正在加载数据表",
+ "Long text": "长文本",
+ "Markdown": "Markdown",
+ "Max": "最大值",
+ "Max length": "最大长度",
+ "Media folders": "媒体文件夹",
+ "Media kind": "媒体类型",
+ "Media library": "媒体库",
+ "Min": "最小值",
+ "Move {0} down": "下移{0}",
+ "Move {0} up": "上移{0}",
+ "Move down": "下移",
+ "Move item {0} down": "下移项目 {0}",
+ "Move item {0} up": "上移项目 {0}",
+ "Move to draft": "移至草稿",
+ "Move up": "上移",
+ "Multi-select": "多选",
+ "Name the value authors will see and the stable key used by templates.": "设置作者看到的名称,以及模板使用的稳定键名。",
+ "New field": "新建字段",
+ "New table": "新建数据表",
+ "No": "否",
+ "No {0} match this view": "没有{0}符合当前视图",
+ "No {0} yet": "暂无{0}",
+ "No content": "无内容",
+ "No items yet": "暂无项目",
+ "No media yet": "暂无媒体",
+ "No other tables available yet.": "暂无其他可用数据表。",
+ "No target table configured": "未配置目标表",
+ "None yet": "暂无",
+ "Number": "数字",
+ "Open editor →": "打开编辑器 →",
+ "Open in Site editor": "在站点编辑器中打开",
+ "Open row": "打开数据行",
+ "Open table": "打开数据表",
+ "Open URL in new tab": "在新标签页打开 URL",
+ "Options": "选项",
+ "Page tree": "页面树",
+ "Percent": "百分比",
+ "Placeholder": "占位提示",
+ "Post type": "文章类型",
+ "Primary field": "主字段",
+ "Product name": "产品名称",
+ "product_name": "product_name",
+ "Project": "项目",
+ "Projects": "项目",
+ "Re-add built-in fields": "重新添加内置字段",
+ "Record structure": "记录结构",
+ "Redirects": "重定向",
+ "Relation": "关联",
+ "Remove \"{0}\" from gallery?": "从图库中移除“{0}”?",
+ "Remove from gallery": "从图库移除",
+ "Remove option": "移除选项",
+ "Repeater": "重复器",
+ "Required": "必填",
+ "Required field — locked": "必填字段——已锁定",
+ "Resize column": "调整列宽",
+ "Rich text": "富文本",
+ "Saving draft": "正在保存草稿",
+ "Schema update failed": "结构更新失败",
+ "Search": "搜索",
+ "Search {0}…": "搜索{0}…",
+ "Search…": "搜索…",
+ "Select": "选择",
+ "Select a table": "选择数据表",
+ "Select a table…": "选择数据表…",
+ "Select all rows": "选择所有行",
+ "Select none": "全部取消",
+ "Select one or several files from the library.": "从媒体库选择一个或多个文件。",
+ "Select row {0}": "选择第 {0} 行",
+ "Set {0} as primary field": "将{0}设为主字段",
+ "Set a target table on the relation field to pick rows.": "请先为关联字段设置目标表,才能选择数据行。",
+ "Set as primary field": "设为主字段",
+ "Set the expectations and guidance shown when someone edits a record.": "设置编辑记录时显示的要求和指引。",
+ "Shown next to the field in the editor": "显示在编辑器字段旁",
+ "Sorted by {0} {1} — click to clear": "按 {0} {1} 排序——点击清除",
+ "Start with the record structure": "从记录结构开始",
+ "Status: {0}": "状态:{0}",
+ "Step": "步长",
+ "Table creation failed": "数据表创建失败",
+ "Table kind": "数据表类型",
+ "Table settings": "数据表设置",
+ "Target table": "目标表",
+ "Templates": "模板",
+ "The media file stays in the library.": "媒体文件会保留在媒体库中。",
+ "Theme & settings": "主题与设置",
+ "This cannot be undone.": "此操作无法撤销。",
+ "This table has no": "此数据表没有",
+ "This table still has {0} row{1}. Delete the rows first.": "此数据表仍有 {0} 行数据,请先删除这些数据行。",
+ "Try a different search term.": "请尝试其他搜索词。",
+ "Try clearing the search or switching views.": "请尝试清除搜索条件或切换视图。",
+ "Type is fixed after creation.": "类型创建后不可修改。",
+ "unavailable": "不可用",
+ "Unknown export error": "未知导出错误",
+ "Unsaved draft": "未保存的草稿",
+ "Updated": "更新时间",
+ "URL": "URL",
+ "USD": "USD",
+ "Used in entry URLs. Changing it can break existing links.": "用于条目 URL,修改后可能导致现有链接失效。",
+ "Used to label collapsed items. Media, relations, and rich text are skipped.": "用于标记折叠项目。媒体、关联和富文本字段会被跳过。",
+ "value": "值",
+ "Yes": "是",
+ "yet — its structure still exports.": ",但其结构仍会被导出。",
+ "Your browser will save the bundle when it is ready.": "数据包准备好后,浏览器会自动保存。",
+ "· re-imports into a fresh instance identically": "· 可原样重新导入到全新实例",
+ "{0} file{1}": "{0} 个文件",
+ "{0} options": "{0} 个选项",
+ "{0} params": "{0} 个参数",
+ "{0} selected": "已选择 {0} 项",
+ "{0} table actions": "{0} 的数据表操作",
+ "1 param": "1 个参数",
+ "Built-in field on a system table — cannot be deleted": "系统表的内置字段,无法删除",
+ "Bulk row actions": "批量数据行操作",
+ "Cannot delete the primary field": "无法删除主字段",
+ "categories selected": "个分类已选择",
+ "Choose media": "选择媒体",
+ "component": "组件",
+ "Create field": "创建字段",
+ "data": "数据",
+ "Download bundle": "下载数据包",
+ "Exporting…": "正在导出…",
+ "field_{0}": "field_{0}",
+ "Full export": "完整导出",
+ "Import site": "导入站点",
+ "Loading image…": "正在加载图片…",
+ "Loading rows": "正在加载数据行",
+ "Missing media": "媒体缺失",
+ "No media": "无媒体",
+ "Pick {0}": "选择{0}",
+ "Pick relation": "选择关联",
+ "post-type": "内容类型",
+ "projects": "项目",
+ "Replace {0}": "替换{0}",
+ "Required by all post types — cannot be deleted": "所有内容类型均需要此字段,无法删除",
+ "Resize data sidebar": "调整数据侧边栏大小",
+ "Row": "数据行",
+ "Row actions": "数据行操作",
+ "Save field": "保存字段",
+ "Select options…": "选择选项…",
+ "This removes the item and every value inside it.": "这会移除该项目及其中的所有值。",
+ "This will permanently delete {0} row{1} and cannot be undone.": "这会永久删除 {0} 行数据,且无法撤销。",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/media.ts b/src/admin/i18n/locales/zh-CN/media.ts
new file mode 100644
index 000000000..9ceb71039
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/media.ts
@@ -0,0 +1,191 @@
+export const mediaZhCN = {
+ "” is no longer installed. Re-elect to keep new uploads working.": "”已不再安装。请重新选择,以确保新上传正常工作。",
+ "{0} in flight · {1} done{2}": "{0} 个处理中 · {1} 个已完成{2}",
+ "{0} installed delegate{1} — pick one to take over variant generation.": "已安装 {0} 个变体处理插件,请选择一个接管变体生成。",
+ "a matching": "匹配的",
+ "a video": "视频",
+ "Accessibility": "无障碍",
+ "Adapter “": "适配器“",
+ "Add {0}": "添加{0}",
+ "Add tag": "添加标签",
+ "Add tags": "添加标签",
+ "Add to folders": "添加到文件夹",
+ "All files": "所有文件",
+ "Alt text is only applied to image assets in the selection.": "替代文本只会应用于所选内容中的图片资源。",
+ "an image": "图片",
+ "an SVG": "SVG",
+ "Apply to": "应用到",
+ "Applying {0}/{1}…": "正在应用 {0}/{1}…",
+ "asset": "资源",
+ "Asset folders": "资源文件夹",
+ "Asset metadata": "资源元数据",
+ "asset.": "资源。",
+ "Assets larger than 1 MiB — likely page-weight offenders.": "大于 1 MiB 的资源,可能会拖慢页面加载。",
+ "assets selected": "个资源已选择",
+ "Assets whose binary has been swapped via \"Replace file\".": "通过“替换文件”更换过二进制内容的资源。",
+ "Assets with no tags assigned.": "尚未分配标签的资源。",
+ "Back to {0}": "返回{0}",
+ "Backend per role": "各资源类型的后端",
+ "Bulk alt text": "批量替代文本",
+ "Bulk edit · {0} selected": "批量编辑 · 已选择 {0} 项",
+ "Caption": "说明",
+ "Choose different file": "选择其他文件",
+ "Choose replacement…": "选择替换文件…",
+ "Clear {0}": "清除{0}",
+ "Click + to create your first folder.": "点击 + 创建第一个文件夹。",
+ "Close picker": "关闭选择器",
+ "Copy public URL": "复制公开 URL",
+ "Current file": "当前文件",
+ "Delete permanently": "永久删除",
+ "Describe the image for screen readers": "为屏幕阅读器描述图片内容",
+ "Dimensions": "尺寸",
+ "Don't change": "不更改",
+ "Drag files into this window or click Upload.": "将文件拖入此窗口,或点击“上传”。",
+ "Drop files onto the media canvas to upload them.": "将文件拖到媒体画布即可上传。",
+ "Duration": "时长",
+ "Edit {0} in viewer": "在查看器中编辑{0}",
+ "Edit asset (alt text, caption, tags…)": "编辑资源(替代文本、说明、标签…)",
+ "Edits apply to all": "编辑会应用到全部",
+ "Failed": "失败",
+ "Failed{0}{1}": "失败{0}{1}",
+ "Filename": "文件名",
+ "Folder assignment failed": "文件夹分配失败",
+ "Folder name": "文件夹名称",
+ "Folders": "文件夹",
+ "from the grid": "从网格中",
+ "Image assets with no title — the filename leaks into the UI.": "没有标题的图片资源,界面会直接显示文件名。",
+ "Image assets without a written alt text.": "未填写替代文本的图片资源。",
+ "Images": "图片",
+ "In Trash since": "移入回收站时间",
+ "Installed adapters": "已安装适配器",
+ "Large files": "大文件",
+ "Largest": "从大到小",
+ "Last error:": "最近错误:",
+ "Leave existing alt text untouched": "保留现有替代文本",
+ "Library": "媒体库",
+ "Loading media": "正在加载媒体",
+ "Matching folders": "匹配的文件夹",
+ "Media metadata and folder edits are read-only for your role.": "你的角色只能查看媒体元数据和文件夹,不能编辑。",
+ "Media panel dock": "媒体面板停靠区",
+ "Migrate": "迁移",
+ "Migrated": "已迁移",
+ "Migrating…": "正在迁移…",
+ "Migration failed:": "迁移失败:",
+ "Migration request failed": "迁移请求失败",
+ "Missing alt text": "缺少替代文本",
+ "Missing title": "缺少标题",
+ "Move existing": "迁移现有文件",
+ "Move to Trash": "移至回收站",
+ "Name A→Z": "名称 A→Z",
+ "Name Z→A": "名称 Z→A",
+ "New file:": "新文件:",
+ "New folder": "新建文件夹",
+ "New folder name": "新文件夹名称",
+ "New root folder": "新建根文件夹",
+ "Newest": "最新",
+ "No assets have been uploaded yet.": "尚未上传资源。",
+ "No external storage adapters installed. The built-in local-disk adapter handles every role until a plugin (S3, R2, …) is installed.": "未安装外部存储适配器。在安装 S3、R2 等插件前,内置本地磁盘适配器会处理所有资源类型。",
+ "No folders have been created yet.": "尚未创建文件夹。",
+ "No folders yet": "暂无文件夹",
+ "No matching media": "没有匹配的媒体",
+ "No uploads yet": "暂无上传任务",
+ "No variant delegate plugins installed yet.": "尚未安装变体处理插件。",
+ "Nothing here yet": "这里暂无内容",
+ "OK": "确定",
+ "Oldest": "最早",
+ "one": "一个",
+ "Open": "打开",
+ "Open {0}": "打开{0}",
+ "Open {0} in a new tab": "在新标签页打开{0}",
+ "Open folder {0}": "打开文件夹{0}",
+ "Optional caption": "可选说明",
+ "originals": "原始文件",
+ "Other": "其他",
+ "parent folder": "父文件夹",
+ "Parent folder": "父文件夹",
+ "pending": "等待中",
+ "pending →": "等待中 →",
+ "Queued": "已排队",
+ "Reads dispatch through the adapter that wrote each asset, so changing the elected backend never strands existing rows.": "读取请求会交给最初写入各资源的适配器,因此更换选定后端不会让现有资源失效。",
+ "Recently replaced": "最近替换",
+ "Remove from folders": "从文件夹移除",
+ "Remove from queue": "从队列移除",
+ "Remove tags": "移除标签",
+ "Rename folder": "重命名文件夹",
+ "Rename media": "重命名媒体",
+ "Replace": "替换",
+ "Replace file": "替换文件",
+ "Replaced": "已替换",
+ "Reset": "重置",
+ "Restore": "恢复",
+ "Retry ·": "重试 ·",
+ "Retry upload": "重试上传",
+ "Search folders…": "搜索文件夹…",
+ "selected — pick": "个已选择——请选择",
+ "selected items. Tag changes are union/diff — adds merge with each asset's existing tags, removes only drop matching tags.": "个所选项目。标签更改采用合并/差异方式:新增标签会与各资源现有标签合并,移除操作只删除匹配标签。",
+ "Selected tags": "已选标签",
+ "Server closed the migration stream without progress.": "服务器在没有迁移进度的情况下关闭了迁移流。",
+ "Show": "显示",
+ "Show {0} in folder": "在文件夹中显示{0}",
+ "Size": "大小",
+ "Smallest": "从小到大",
+ "Soft-deleted assets show up here.": "软删除的资源会显示在这里。",
+ "Sort media": "媒体排序",
+ "Storage adapter for {0}": "{0}的存储适配器",
+ "SVG": "SVG",
+ "Tag suggestions": "标签建议",
+ "Tags": "标签",
+ "Tags to add": "要添加的标签",
+ "Tags to remove": "要移除的标签",
+ "Testing…": "正在测试…",
+ "The new file inherits the same public URL — every page and content entry that already references this asset will switch to the new binary instantly. The previous file is removed from disk.": "新文件会沿用相同的公开 URL;所有引用此资源的页面和内容条目会立即切换到新文件,旧文件将从磁盘移除。",
+ "This is not": "这不是",
+ "to the elected backend.": "到选定的后端。",
+ "Toggle upload queue": "切换上传队列",
+ "Trash": "回收站",
+ "Trash is empty": "回收站为空",
+ "Try a different search or filter.": "请尝试其他搜索词或筛选条件。",
+ "Uncategorized": "未分类",
+ "Unknown type": "未知类型",
+ "Unresolved {0}": "未解析:{0}",
+ "Untagged": "无标签",
+ "Uploaded": "已上传",
+ "Uploading": "正在上传",
+ "Uploads": "上传",
+ "Uploads ({0}/{1})": "上传({0}/{1})",
+ "Use {0} selected": "使用已选的 {0} 项",
+ "Use selected": "使用所选内容",
+ "Variant delegate": "变体处理插件",
+ "variants": "变体",
+ "Verify request failed": "验证请求失败",
+ "Videos": "视频",
+ "Viewer: {0}": "查看器:{0}",
+ "When elected, the host skips local image resizing and emits responsive variant URLs from the delegate's template.": "选用后,主机会跳过本地图像缩放,并根据该插件模板生成响应式变体 URL。",
+ "Bulk edit selected media": "批量编辑所选媒体",
+ "Clear finished uploads": "清除已完成的上传",
+ "Click to edit this asset (alt text, caption, tags…)": "点击编辑此资源(替代文本、说明、标签等)",
+ "Folder options": "文件夹选项",
+ "Folder tree": "文件夹树",
+ "image": "图片",
+ "Loading media storage": "正在加载媒体存储",
+ "Local disk (built-in)": "本地磁盘(内置)",
+ "Local sharp ladder (built-in)": "本地 Sharp 响应式尺寸(内置)",
+ "Media item options": "媒体项目选项",
+ "Media view": "媒体视图",
+ "New subfolder": "新建子文件夹",
+ "Open the media library to {0} {1}": "打开媒体库以{0}{1}",
+ "Replacing…": "正在替换…",
+ "Resize media sidebar": "调整媒体侧边栏大小",
+ "Search media": "搜索媒体",
+ "Select a video": "选择视频",
+ "Select an image": "选择图片",
+ "Select an SVG": "选择 SVG",
+ "Select media": "选择媒体",
+ "smart:missing-title": "smart:missing-title",
+ "Sort {0}": "按{0}排序",
+ "System folders": "系统文件夹",
+ "Upload": "上传",
+ "Upload failed with {0}": "上传失败:{0}",
+ "Upload queue": "上传队列",
+ "video": "视频",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/modals.ts b/src/admin/i18n/locales/zh-CN/modals.ts
new file mode 100644
index 000000000..69bdba669
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/modals.ts
@@ -0,0 +1,206 @@
+export const modalsZhCN = {
+ "…and": "…以及",
+ "(and rewrites the imported CSS to match); skip keeps your current token; overwrite replaces your token's value.": "(并同步改写导入的 CSS);跳过会保留当前 Token;覆盖会替换当前 Token 的值。",
+ "{0} (not in current site)": "{0}(当前站点中不存在)",
+ "{0} pages · {1} style rules · {2} assets": "{0} 个页面 · {1} 条样式规则 · {2} 个资源",
+ "← Back": "← 返回",
+ "A short description of your website.": "网站的简短描述。",
+ "Add import files": "添加导入文件",
+ "Add more files": "添加更多文件",
+ "All keyboard shortcuts, organized by context. Platform-specific hints are shown automatically based on your operating system.": "按使用场景整理的全部键盘快捷键。系统会根据你的操作系统自动显示对应提示。",
+ "Back": "返回",
+ "Browse library…": "浏览媒体库…",
+ "Browse media library for favicon": "从媒体库选择站点图标",
+ "Bulk class name conflict actions": "批量处理类名冲突",
+ "Bulk cross-stylesheet conflict actions": "批量处理跨样式表冲突",
+ "Bulk design token conflict actions": "批量处理设计 Token 冲突",
+ "Bulk page slug conflict actions": "批量处理页面别名冲突",
+ "Bundle": "数据包",
+ "Can’t import": "无法导入",
+ "Can’t import": "无法导入",
+ "Cancel current schedule": "取消当前计划",
+ "Change favicon": "更换站点图标",
+ "Checking bundle against current site…": "正在将数据包与当前站点进行比对…",
+ "Choose a different file": "选择其他文件",
+ "Choose files": "选择文件",
+ "Choose folder": "选择文件夹",
+ "Class name conflicts (": "类名冲突(",
+ "Clear command history": "清除命令历史",
+ "Clear favicon": "清除站点图标",
+ "close": "关闭",
+ "Close": "关闭",
+ "Collapse {0}": "折叠{0}",
+ "Color tokens": "颜色 Token",
+ "Conflict resolution for {0}": "{0}的冲突解决方案",
+ "Conflicts": "冲突",
+ "Continue →": "继续 →",
+ "Converted to editable style rules · used by {0} {1}": "已转换为可编辑的样式规则 · 被 {0} 个{1}使用",
+ "Could not add files": "无法添加文件",
+ "Custom": "自定义",
+ "Database": "数据库",
+ "Design token conflicts (": "设计 Token 冲突(",
+ "Draft source": "草稿来源",
+ "Drop": "放下",
+ "Drop a site folder, CMS bundle, or .zip here": "将站点文件夹、CMS 数据包或 .zip 拖到这里",
+ "Drop another bundle, HTML, media, or browse": "拖入其他数据包、HTML、媒体,或浏览文件",
+ "Drop HTML, CSS, JS or assets, or browse": "拖入 HTML、CSS、JS 或资源,或浏览文件",
+ "Drop site files, a folder, a CMS bundle, or a .zip archive here": "将站点文件、文件夹、CMS 数据包或 .zip 压缩包拖到这里",
+ "Drop to add files": "放下以添加文件",
+ "Editor preferences are stored locally on this device and do not affect the site file.": "编辑器偏好只存储在此设备,不会影响站点文件。",
+ "Emit only generated color, typography, and spacing utility classes used in the page and component trees. Turn this off when custom runtime code references generated utilities outside the editor tree.": "仅输出页面树和组件树中使用的颜色、排版和间距实用类。如果自定义运行时代码在编辑器树之外引用了生成类,请关闭此选项。",
+ "en": "英文",
+ "Esc": "Esc",
+ "Expand {0}": "展开{0}",
+ "Exported": "已导出",
+ "Failed to cancel schedule": "取消计划失败",
+ "Failed to load site settings": "加载站点设置失败",
+ "Failed to preview bundle": "预览数据包失败",
+ "Failed to read CMS bundle": "读取 CMS 数据包失败",
+ "Failed to save site settings": "保存站点设置失败",
+ "Failed to schedule publish": "计划发布失败",
+ "Favicon": "站点图标",
+ "file": "个文件",
+ "files": "个文件",
+ "folder assignment failed": "文件夹分配失败",
+ "Fonts": "字体",
+ "Framework CSS": "框架 CSS",
+ "From {0}": "来自{0}",
+ "From site": "来自站点",
+ "Google font ·": "Google 字体 ·",
+ "Hide import log": "隐藏导入日志",
+ "HTML, CSS, images, fonts, and CMS bundles are supported": "支持 HTML、CSS、图片、字体和 CMS 数据包",
+ "Import": "导入",
+ "Import complete": "导入完成",
+ "Import didn’t finish": "导入未完成",
+ "Import failed": "导入失败",
+ "Import log": "导入日志",
+ "Import mode": "导入模式",
+ "Import mode for {0}": "{0}的导入模式",
+ "Import progress": "导入进度",
+ "Imported": "已导入",
+ "Imported as a stylesheet file, scoped to {0} {1}": "已作为样式表文件导入,作用域为 {0}{1}",
+ "Imported into": "已导入到",
+ "Importing into": "正在导入到",
+ "in bundle": "在数据包中",
+ "Include all rules in {0}": "包含{0}中的所有规则",
+ "Include font {0}": "包含字体 {0}",
+ "Include page {0}": "包含页面 {0}",
+ "Include rule {0}": "包含规则 {0}",
+ "Include script {0}": "包含脚本 {0}",
+ "Include stylesheet {0}": "包含样式表 {0}",
+ "Ingesting files and analyzing…": "正在读取并分析文件…",
+ "Insert bundle rows that do not exist locally. Existing rows stay untouched.": "插入本地不存在的数据包记录,现有记录保持不变。",
+ "is ready": "已准备就绪",
+ "Keep first all": "全部保留首个定义",
+ "Keep the first definition for all cross-stylesheet conflicts": "所有跨样式表冲突均保留首个定义",
+ "Keep this window open while importing…": "导入期间请保持此窗口打开…",
+ "Language": "语言",
+ "media selected": "个媒体已选择",
+ "Merge: add only": "合并:仅新增",
+ "Merge: overwrite": "合并:覆盖",
+ "Meta Description": "元描述",
+ "Meta Title": "元标题",
+ "more": "更多",
+ "more in": "更多内容位于",
+ "My Website": "我的网站",
+ "No colour tokens found.": "未找到颜色 Token。",
+ "No content in this bundle.": "此数据包中没有内容。",
+ "No favicon selected": "未选择站点图标",
+ "No imported nodes": "没有导入的节点",
+ "No installable fonts or font tokens in this import.": "此次导入中没有可安装字体或字体 Token。",
+ "No media files in this bundle.": "此数据包中没有媒体文件。",
+ "No media files in this import.": "此次导入中没有媒体文件。",
+ "No options available": "没有可用选项",
+ "No rows in this table.": "此数据表中没有数据行。",
+ "No scripts in this import.": "此次导入中没有脚本。",
+ "node": "个节点",
+ "nodes": "个节点",
+ "Nothing was skipped; everything imports cleanly.": "没有跳过任何内容,所有内容均可顺利导入。",
+ "Open site →": "打开站点 →",
+ "Overwrite": "覆盖",
+ "Overwrite all": "全部覆盖",
+ "Overwrite all class name conflicts": "覆盖所有类名冲突",
+ "Overwrite all design token conflicts": "覆盖所有设计 Token 冲突",
+ "Overwrite all page slug conflicts": "覆盖所有页面别名冲突",
+ "page": "个页面",
+ "Page slug conflicts (": "页面别名冲突(",
+ "page-slug": "页面别名",
+ "pages": "个页面",
+ "Preferences": "偏好设置",
+ "Progress": "进度",
+ "Published pages are served by this self-hosted CMS.": "已发布页面由此自托管 CMS 提供。",
+ "Publishing": "发布",
+ "Removes the list of recently run commands shown when the palette opens with an empty query, and erases any local usage counts.": "清除命令面板在空搜索时显示的最近命令列表,并删除本地使用次数。",
+ "Rename": "重命名",
+ "Rename all": "全部重命名",
+ "Rename all class name conflicts": "重命名所有类名冲突",
+ "Rename all cross-stylesheet conflicts": "重命名所有跨样式表冲突",
+ "Rename all design token conflicts": "重命名所有设计 Token 冲突",
+ "Rename all page slug conflicts": "重命名所有页面别名冲突",
+ "Rename with a numeric suffix": "使用数字后缀重命名",
+ "Replace everything": "替换全部内容",
+ "Reschedule this {0}": "重新计划此{0}",
+ "Review": "检查",
+ "Route for {0}": "{0}的路由",
+ "row": "条数据行",
+ "Row slug conflicts (": "数据行别名冲突(",
+ "rows": "条数据行",
+ "Rows": "数据行",
+ "rule": "条规则",
+ "rules": "条规则",
+ "rules ·": "条规则 ·",
+ "Runtime": "运行时",
+ "Saved path": "保存路径",
+ "Schedule this {0}": "计划发布此{0}",
+ "Scheduled time must be in the future.": "计划时间必须晚于当前时间。",
+ "Scripts": "脚本",
+ "Search selectors…": "搜索选择器…",
+ "Settings keyboard shortcuts": "设置键盘快捷键",
+ "Settings sections": "设置分区",
+ "Shortcuts": "快捷键",
+ "Site imported": "站点已导入",
+ "Site Name": "站点名称",
+ "Site name and HTML metadata used by the published CMS pages.": "已发布 CMS 页面使用的站点名称和 HTML 元数据。",
+ "Site-level configuration. Press Escape to close.": "站点级配置。按 Escape 关闭。",
+ "Skip": "跳过",
+ "Skip all": "全部跳过",
+ "Skip all class name conflicts": "跳过所有类名冲突",
+ "Skip all design token conflicts": "跳过所有设计 Token 冲突",
+ "Skip all page slug conflicts": "跳过所有页面别名冲突",
+ "Something went wrong while importing. No changes were applied.": "导入时出现错误,未应用任何更改。",
+ "Style rules": "样式规则",
+ "Stylesheets disagree (": "样式表冲突(",
+ "These class names are already used in this site's style registry.": "这些类名已在当前站点的样式注册表中使用。",
+ "These colour / font variables already exist in this site. Rename keeps the imported value on a new": "这些颜色或字体变量已存在于当前站点。重命名会将导入值保留在新的",
+ "These imported rows use a slug that already exists in the target table. Rename the imported row or skip it before continuing.": "这些导入数据行使用了目标表中已有的别名。继续前请重命名导入数据行或将其跳过。",
+ "These pages share a slug with an existing page, or with another page in this import. Choose how to resolve each one.": "这些页面与现有页面或此次导入中的其他页面使用了相同别名。请选择每项冲突的处理方式。",
+ "Tree-shake generated framework utilities": "移除未使用的框架实用类",
+ "Two imported stylesheets define the same class differently. Rename keeps each page faithful to its own stylesheet (the listed pages move to the new name); skip uses the first definition everywhere; overwrite makes this definition win the original name.": "两个导入样式表对同一个类有不同定义。重命名会让各页面继续使用各自样式表(列出的页面改用新名称);跳过会统一使用第一个定义;覆盖会让此定义占用原名称。",
+ "Unknown import error": "未知导入错误",
+ "Upsert bundle rows. Local rows that are not in the bundle stay untouched.": "新增或更新数据包中的记录;数据包之外的本地记录保持不变。",
+ "variant": "变体",
+ "View import log": "查看导入日志",
+ "Wipe the local site and replace it with the bundle. Best for full restores.": "清空本地站点并使用数据包替换,适合完整恢复。",
+ "Add rows": "添加数据行",
+ "Attached where the source HTML linked them": "按源 HTML 中的链接位置附加",
+ "Choose how this bundle lands in the current site": "选择如何将此数据包导入当前站点",
+ "Confirm which pages import and set their routes": "确认要导入的页面并设置路由",
+ "Dropped; your pages are unaffected": "已丢弃;你的页面不受影响",
+ "Editable style rules": "可编辑的样式规则",
+ "GIF": "GIF",
+ "Imported {0} {1}": "已导入 {0} {1}",
+ "Imported into the Media library": "已导入媒体库",
+ "Imported node preview": "导入节点预览",
+ "Importing...": "正在导入...",
+ "Installed families and root font variables": "已安装字体系列和根字体变量",
+ "Keep as stylesheet": "保留为样式表",
+ "Loading site settings": "正在加载站点设置",
+ "Overwrite rows": "覆盖数据行",
+ "Pages from the exported site": "导出站点中的页面",
+ "Pick how each stylesheet imports — editable rules, or a file kept as-is": "选择每个样式表的导入方式:可编辑规则,或原样保留文件",
+ "Replace site": "替换站点",
+ "Root custom properties become palette tokens": "根级自定义属性将转换为调色板令牌",
+ "Rows from this exported table": "此导出数据表中的数据行",
+ "Schedule when to publish this {0}": "安排何时发布此{0}",
+ "Uploaded to the Media library": "已上传到媒体库",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/plugins.ts b/src/admin/i18n/locales/zh-CN/plugins.ts
new file mode 100644
index 000000000..ea8847826
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/plugins.ts
@@ -0,0 +1,139 @@
+export const pluginsZhCN = {
+ "Already approved": "已批准",
+ "Completed": "已完成",
+ "Daily at {0} UTC": "每天 UTC {0}",
+ "Every {0} minute{1}": "每 {0} 分钟",
+ "Every hour": "每小时",
+ "existing": "现有",
+ "Loading plugin app": "正在加载插件应用",
+ "Monthly on day {0} at {1} UTC": "每月 {0} 日 UTC {1}",
+ "new": "新增",
+ "No longer requested": "不再请求",
+ "Plugin editor entrypoint failed to load.": "插件编辑器入口加载失败。",
+ "Plugin file": "插件文件",
+ "Remove plugin": "移除插件",
+ "Save settings": "保存设置",
+ "Saving...": "正在保存…",
+ "Timed out": "已超时",
+ "Upload Plugin": "上传插件",
+ "Weekly on {0} at {1} UTC": "每周{0} UTC {1}",
+ "— (cancelled)": "—(已取消)",
+ "— (paused)": "—(已暂停)",
+ ". Review the highlighted rows below before continuing.": "。继续前请检查下方高亮的数据行。",
+ ". That code has the same access as the admin UI itself — your admin session, every admin API, and this browser tab. Only continue if you trust the plugin author.": "。该代码拥有与后台界面相同的访问权限,包括你的管理员会话、所有后台 API 和当前浏览器标签页。仅在信任插件作者时继续。",
+ "(admin app pages)": "(后台应用页面)",
+ "(an editor entrypoint and admin app pages)": "(编辑器入口和后台应用页面)",
+ "(an editor entrypoint)": "(编辑器入口)",
+ "{0} is ready to install.": "{0} 已准备好安装。",
+ "{0} item(s) installed, {1} replaced.": "已安装 {0} 项,替换 {1} 项。",
+ "{0} records": "{0} 条记录",
+ "{0} requests access before activation.": "{0} 在激活前请求以下权限。",
+ "{0} v{1}": "{0} v{1}",
+ "{0}ms": "{0} 毫秒",
+ "Action failed": "操作失败",
+ "Active": "已启用",
+ "and": "和",
+ "Approve {0} new and update to {1}": "批准 {0} 项新增权限并更新到 {1}",
+ "Approve and Install": "批准并安装",
+ "Auto-respawning (crash #{0} in 5min). Reason: {1}": "正在自动重启(5 分钟内第 {0} 次崩溃)。原因:{1}",
+ "by": "作者",
+ "Cadence": "频率",
+ "Cancelled": "已取消",
+ "Could not load schedules": "无法加载计划任务",
+ "Could not load settings": "无法加载设置",
+ "Could not remove plugin": "无法移除插件",
+ "Could not save settings": "无法保存设置",
+ "Crashed {0} times in 5min. Open the Plugins page to restart.": "5 分钟内崩溃 {0} 次。请打开插件页面进行重启。",
+ "Create {0}": "创建{0}",
+ "Creating": "正在创建",
+ "deactivate": "停用",
+ "Delete": "删除",
+ "Delete {0}": "删除{0}",
+ "Delete every record stored under the plugin’s declared resources.": "删除该插件声明资源下存储的所有记录。",
+ "directly in the admin window, outside the plugin sandbox": "直接运行在后台窗口中,不受插件沙箱保护",
+ "Disable": "禁用",
+ "Disabled": "已禁用",
+ "Drop its routes, hooks, settings, and canvas modules from the runtime.": "从运行时移除其路由、钩子、设置和画布模块。",
+ "Drop its routes, hooks, settings, schedules, and canvas modules from the runtime.": "从运行时移除其路由、钩子、设置、计划任务和画布模块。",
+ "Edit settings for {0}": "编辑{0}的设置",
+ "Editor:": "编辑器:",
+ "Enable": "启用",
+ "Error": "错误",
+ "External hosts": "外部主机",
+ "External resources the plugin set up — webhooks, third-party registrations, scheduled callbacks — may be left behind, because the plugin’s cleanup code does not run.": "由于不会运行插件清理代码,插件创建的外部资源(Webhook、第三方注册、计划回调)可能会残留。",
+ "Failed to save settings": "保存设置失败",
+ "Failures": "失败次数",
+ "for what's allowed inside plugin code.": "了解插件代码中允许执行的操作。",
+ "Healthy": "正常",
+ "Homepage": "主页",
+ "in a row": "次连续失败",
+ "Install admin extensions and control what they add to the CMS.": "安装后台扩展,并控制它们向 CMS 添加的内容。",
+ "Installed": "已安装",
+ "Installed pack from {0}": "已从 {0} 安装资源包",
+ "Installed plugins": "已安装插件",
+ "Installing": "正在安装",
+ "Last run": "上次运行",
+ "lifecycle hooks.": "生命周期钩子。",
+ "Loading plugin": "正在加载插件",
+ "Loading records": "正在加载记录",
+ "New": "新增",
+ "new permission": "项新权限",
+ "Next run": "下次运行",
+ "No new permissions in this update.": "此次更新没有新增权限。",
+ "No permissions requested — this plugin is purely declarative and gets no access to CMS data, editor state, or the network.": "未请求权限。该插件为纯声明式插件,无法访问 CMS 数据、编辑器状态或网络。",
+ "No records yet.": "暂无记录。",
+ "No schedules registered": "未注册计划任务",
+ "No settings declared": "未声明设置项",
+ "Pack-imported Visual Components, pages, and CSS classes stay on your site.": "通过资源包导入的可视化组件、页面和 CSS 类会保留在站点中。",
+ "Pause": "暂停",
+ "Paused": "已暂停",
+ "Pending": "等待中",
+ "Plugin \"{0}\" crashed": "插件“{0}”已崩溃",
+ "Plugin \"{0}\" parked in error state": "插件“{0}”已停留在错误状态",
+ "Plugin page": "插件页面",
+ "Plugin page unavailable": "插件页面不可用",
+ "Plugin resource unavailable.": "插件资源不可用。",
+ "Re-sync {0} pack from the plugin's latest version": "从插件最新版本重新同步{0}资源包",
+ "Re-sync pack": "重新同步资源包",
+ "Recent issues (": "近期问题(",
+ "Recent runs": "近期运行记录",
+ "Reinstall": "重新安装",
+ "Reinstall {0} — upload a new version to replace the broken install": "重新安装{0}——上传新版本以替换损坏的安装",
+ "Remove": "移除",
+ "Remove {0}": "移除{0}",
+ "Remove all of the plugin’s files under": "移除插件在以下位置的全部文件:",
+ "Remove anyway": "仍要移除",
+ "Remove the plugin’s files from": "从以下位置移除插件文件:",
+ "Removing anyway skips the plugin’s cleanup code — external resources it created (webhooks, third-party registrations) may remain.": "强制移除会跳过插件清理代码;其创建的外部资源(Webhook、第三方注册)可能仍会保留。",
+ "Removing this plugin will:": "移除此插件将会:",
+ "Restart": "重启",
+ "Restart {0}": "重启{0}",
+ "Resume": "恢复",
+ "Review {0}": "检查{0}",
+ "Run its": "运行其",
+ "Run now": "立即运行",
+ "sandbox documentation": "沙箱文档",
+ "Schedules": "计划任务",
+ "Secrets need re-entry": "需要重新输入密钥",
+ "Settings": "设置",
+ "Skip its": "跳过其",
+ "Source": "来源",
+ "The plugin may be disabled, removed, or using a page that no longer exists.": "该插件可能已禁用、移除,或使用了已不存在的页面。",
+ "The plugin will connect to these hosts from the server and from published pages. Hosts not listed here are blocked.": "插件会从服务器和已发布页面连接这些主机;未列出的主机会被阻止。",
+ "The plugin’s own cleanup code will be skipped. Force-removing will:": "将跳过插件自身的清理代码。强制移除会:",
+ "The server's encryption key changed since these values were saved. Re-enter: {0}.": "保存这些值后服务器加密密钥发生了变化。请重新输入:{0}。",
+ "This looks like a plugin sandbox issue. See the": "这似乎是插件沙箱问题,请参阅",
+ "This plugin runs its own JavaScript": "此插件会运行自己的 JavaScript",
+ "This update requests": "此次更新请求",
+ "Timeout": "超时",
+ "uninstall": "卸载",
+ "Update {0}": "更新{0}",
+ "Update to {0}": "更新到 {0}",
+ "Updating": "正在更新",
+ "Updating from {0} to {1}. Existing settings and stored data are preserved; the plugin runs its migrate hook before re-activating.": "正在从 {0} 更新到 {1}。现有设置和存储数据将保留;插件会在重新激活前运行迁移钩子。",
+ "uploads/plugins/": "uploads/plugins/",
+ "uploads/plugins/…": "uploads/plugins/…",
+ "Version {0}": "版本 {0}",
+ "View schedules for {0}": "查看{0}的计划任务",
+ "Working...": "正在处理…",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/shared.ts b/src/admin/i18n/locales/zh-CN/shared.ts
new file mode 100644
index 000000000..17cd14208
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/shared.ts
@@ -0,0 +1,240 @@
+export const sharedZhCN = {
+ "Account menu": "账户菜单",
+ "Add missing tokens": "添加缺失的 Token",
+ "Applying…": "正在应用…",
+ "Cannot bind a {0} field to a {1} control": "无法将{0}字段绑定到{1}控件",
+ "Cannot bind this {0} field to a {1} control": "无法将此{0}字段绑定到{1}控件",
+ "Cannot bind this loop field to a {0} control": "无法将此循环字段绑定到{0}控件",
+ "Component · {0}": "组件 · {0}",
+ "Current entry → {0}": "当前条目 → {0}",
+ "Dock in {0}": "停靠在{0}",
+ "Editor chunk failed to load": "编辑器模块加载失败",
+ "Import framework": "导入框架",
+ "Insert": "插入",
+ "inspector": "检查器",
+ "Instatic": "Instatic",
+ "Just the :root custom properties — bring your own classes and CSS.": "仅使用 :root 自定义属性;类和 CSS 由你自行提供。",
+ "Open live page": "打开线上页面",
+ "Open live site": "打开线上站点",
+ "Remove framework": "移除框架",
+ "Remove the Core Framework entirely — every variable and generated class.": "彻底移除核心框架,包括所有变量和生成类。",
+ "Resize right sidebar": "调整右侧边栏大小",
+ "Search models…": "搜索模型…",
+ "settings": "设置",
+ "step_up_cancelled": "step_up_cancelled",
+ "Switch to variables": "切换为仅变量",
+ "Unpin to floating panel": "取消固定为浮动面板",
+ "Up to date": "已是最新",
+ "Update framework": "更新框架",
+ "Utility classes + variables. The complete Core Framework, ready to use on the canvas.": "实用类与变量。完整核心框架,可直接在画布中使用。",
+ "{0} fields": "{0} 个字段",
+ "{0} panel": "{0}面板",
+ "{0} panel header": "{0}面板标题栏",
+ "{0} plugin{1} in error state": "{0} 个插件处于错误状态",
+ "{0} plugin{1} need{2} attention": "{0} 个插件需要处理",
+ "{0} uses": "{0} 处使用",
+ "1 use": "1 处使用",
+ "about": "关于",
+ "About": "关于",
+ "Access unavailable": "无法访问",
+ "Account & security": "账户与安全",
+ "Account menu for {0}": "{0} 的账户菜单",
+ "Add, remove, move, and rename nodes; manage pages, components, and classes.": "添加、移除、移动和重命名节点;管理页面、组件和类。",
+ "Allow AI write tools": "允许 AI 写入工具",
+ "analytics": "分析",
+ "Applies to": "应用于",
+ "Authentication or recovery code": "验证码或恢复码",
+ "Browse custom tables": "浏览自定义表",
+ "Browse installed plugins": "浏览已安装插件",
+ "Browse media library": "浏览媒体库",
+ "Browse system tables": "浏览系统表",
+ "Cancel": "取消",
+ "Change text, images, and links on existing nodes — no structure or style changes.": "修改现有节点的文本、图像和链接,但不更改结构或样式。",
+ "class": "类",
+ "class is": "类已",
+ "class itself": "类本身",
+ "classes": "类",
+ "classes are": "类已",
+ "classes themselves": "类本身",
+ "Clear": "清除",
+ "Clear {0} capabilities": "清除 {0} 项权限",
+ "Clear all": "全部清除",
+ "Clear all capabilities": "清除所有权限",
+ "Close {0} panel": "关闭{0}面板",
+ "CMS is unavailable": "CMS 不可用",
+ "Configure AI providers, credentials, and per-scope defaults.": "配置 AI 提供商、凭据和各作用域的默认值。",
+ "Configure plugins": "配置插件",
+ "Confirm": "确认",
+ "Confirm your password": "确认密码",
+ "Confirming…": "正在确认…",
+ "Content": "内容",
+ "Context window": "上下文窗口",
+ "Could not confirm password.": "无法确认密码。",
+ "Could not load CMS site": "无法加载 CMS 站点",
+ "Could not load tables": "无法加载数据表",
+ "Could not update the framework": "无法更新框架",
+ "Create content": "创建内容",
+ "Create new draft posts and content rows.": "创建新的文章草稿和内容行。",
+ "Create, edit, and delete custom roles; assign capabilities to roles.": "创建、编辑和删除自定义角色,并为角色分配权限。",
+ "Create, edit, delete, and suspend users; assign roles.": "创建、编辑、删除和停用用户,并分配角色。",
+ "Create, rename, and delete custom tables; add/edit/remove their fields and route bases.": "创建、重命名和删除自定义表;添加、编辑或移除字段及路由前缀。",
+ "Current URL path and slug. Useful for SEO and breadcrumbs.": "当前 URL 路径和别名,可用于 SEO 和面包屑导航。",
+ "Dashboard": "仪表盘",
+ "Data": "数据",
+ "Delete component": "删除组件",
+ "Delete component?": "删除组件?",
+ "Delete media": "删除媒体",
+ "Download a JSON bundle of tables + rows (plus optional media bytes). Includes the import preview dry-run.": "下载包含数据表和数据行的 JSON 包(可选择包含媒体文件),并支持导入预览试运行。",
+ "Edit any content": "编辑任意内容",
+ "Edit own content": "编辑自己的内容",
+ "Edit page metadata such as title, slug, and SEO fields.": "编辑页面标题、别名和 SEO 字段等元数据。",
+ "Edit pages": "编辑页面",
+ "Edit per-plugin settings and manage plugin-owned records.": "编辑各插件设置并管理插件拥有的记录。",
+ "Edit posts and rows authored by anyone.": "编辑任意作者创建的文章和数据行。",
+ "Edit posts and rows that you authored.": "编辑自己创建的文章和数据行。",
+ "Edit site content": "编辑站点内容",
+ "Edit site structure": "编辑站点结构",
+ "Edit site styles": "编辑站点样式",
+ "Edit the site’s package.json dependencies and trigger resolve/install.": "编辑站点的 package.json 依赖并触发解析或安装。",
+ "Editor is still loading": "编辑器仍在加载",
+ "Elect storage backends": "选择存储后端",
+ "Elect the media storage adapter per asset role (originals / variants / avatars / fonts) and the variant delegate.": "为各资源类型(原图、变体、头像、字体)选择媒体存储适配器和变体处理方。",
+ "element": "元素",
+ "elements": "元素",
+ "Enable, disable, restart plugins. Pause/resume/run-now their schedules. Step-up gated.": "启用、禁用或重启插件;暂停、恢复或立即运行其计划任务。操作需要二次验证。",
+ "Everywhere": "所有位置",
+ "Export data bundles": "导出数据包",
+ "Failed to load data meta": "加载数据元信息失败",
+ "Fields of the page currently being rendered.": "当前正在渲染页面的字段。",
+ "Framework state": "框架状态",
+ "from every element below. The": "从下方所有元素中移除。该",
+ "Full admin: manage every content row regardless of author.": "完整管理权限:不受作者限制,可管理所有内容行。",
+ "Full framework": "完整框架",
+ "generated": "已生成",
+ "Hero card": "主视觉卡片",
+ "HTML": "HTML",
+ "HTML source": "HTML 源码",
+ "Import data bundles": "导入数据包",
+ "Import HTML": "导入 HTML",
+ "Import the framework": "导入框架",
+ "In other components": "在其他组件中",
+ "In pages": "在页面中",
+ "Input / output price per 1M tokens": "每百万 Token 的输入/输出价格",
+ "Install or uninstall plugins": "安装或卸载插件",
+ "Install, upgrade, and uninstall plugins. Runs third-party code on the host — RCE-class. Step-up gated.": "安装、升级和卸载插件。此操作会在主机运行第三方代码,具有远程代码执行级风险,需要二次验证。",
+ "instance": "实例",
+ "instances": "实例",
+ "is used in": "使用于",
+ "Lets AI conversations mutate the editor store (insert nodes, edit props, etc.) via the canvas bridge. Without this, the model has no write tools registered.": "允许 AI 对话通过画布桥接修改编辑器状态(插入节点、编辑属性等)。关闭后,模型不会注册任何写入工具。",
+ "Loading {0} canvas frame": "正在加载{0}画布框架",
+ "Loading credentials…": "正在加载凭据…",
+ "Loading editor": "正在加载编辑器",
+ "Loading models…": "正在加载模型…",
+ "Loop metadata": "循环元数据",
+ "Manage AI providers": "管理 AI 提供商",
+ "Manage content": "管理内容",
+ "Manage custom tables": "管理自定义表",
+ "Manage roles": "管理角色",
+ "Manage runtime dependencies": "管理运行时依赖",
+ "Manage system-table custom fields": "管理系统表自定义字段",
+ "Manage the framework": "管理框架",
+ "Manage users": "管理用户",
+ "Media": "媒体",
+ "Migrate storage bytes": "迁移存储文件",
+ "Model": "模型",
+ "Modify CSS classes, style overrides, breakpoints, and framework tokens.": "修改 CSS 类、样式覆盖、断点和框架 Token。",
+ "Move a row from one table to another (changes public URL because route base differs per table).": "将数据行从一个表移至另一个表(各表路由前缀不同,因此公开 URL 会改变)。",
+ "Move rows between tables": "在数据表之间移动数据行",
+ "Name": "名称",
+ "New component": "新建组件",
+ "New page": "新建页面",
+ "New script": "新建脚本",
+ "New stylesheet": "新建样式表",
+ "No context menu available": "没有可用的上下文菜单",
+ "No credentials yet": "暂无凭据",
+ "No fields in the available sources are compatible with this control.": "可用数据源中没有与此控件兼容的字段。",
+ "No matches": "没有匹配项",
+ "No models available": "没有可用模型",
+ "None": "无",
+ "Not found (404)": "未找到(404)",
+ "of": "共",
+ "On system tables: add/edit/remove custom fields and choose the primary field. Built-in fields and the table identity (name, slug, route) stay locked.": "在系统表中添加、编辑或移除自定义字段,并选择主字段。内置字段和表标识(名称、别名、路由)保持锁定。",
+ "Open {0} panel": "打开{0}面板",
+ "Open AI conversations and use read-only tools (snapshot, search). Cannot mutate state without `Allow AI write tools`.": "打开 AI 对话并使用只读工具(快照、搜索)。若未授予“允许 AI 写入工具”,则不能修改状态。",
+ "Open the Dashboard workspace and see activity widgets.": "打开仪表盘工作区并查看活动组件。",
+ "Open the Data workspace; browse user-created (custom) tables and their field schemas. Does not reveal the internal system tables.": "打开数据工作区,浏览用户创建的自定义表及字段结构,不会显示内部系统表。",
+ "Open the Media workspace, browse assets and folders, see thumbnails in pickers.": "打开媒体工作区,浏览资源和文件夹,并在选择器中查看缩略图。",
+ "Open the Site workspace; view pages, components, and classes.": "打开站点工作区,查看页面、组件和类。",
+ "Other components using this component": "使用此组件的其他组件",
+ "Overwrite the bytes of an existing asset (variants regenerate). Uniquely powerful — silently swaps the file every reference points at.": "覆盖现有资源文件(会重新生成变体)。此权限风险较高,会直接替换所有引用所指向的文件。",
+ "Page": "页面",
+ "Page settings": "页面设置",
+ "Page title": "页面标题",
+ "Pages using this component": "使用此组件的页面",
+ "Parent slug": "父级别名",
+ "Password": "密码",
+ "Path": "路径",
+ "Permalink": "固定链接",
+ "Plugin lifecycle control": "插件生命周期控制",
+ "Plugins": "插件",
+ "Post types": "文章类型",
+ "Priority": "优先级",
+ "Proceeding will delete the component and remove every reference from the site.": "继续操作将删除该组件,并移除站点中的所有引用。",
+ "Proceeding will remove the": "继续操作将移除该",
+ "Publish any content": "发布任意内容",
+ "Publish or unpublish pages to the live site.": "将页面发布到线上站点或取消发布。",
+ "Publish own content": "发布自己的内容",
+ "Publish pages": "发布页面",
+ "Publish posts and rows authored by anyone.": "发布任意作者创建的文章和数据行。",
+ "Publish posts and rows that you authored.": "发布自己创建的文章和数据行。",
+ "Read AI audit log": "读取 AI 审计日志",
+ "Read audit log": "读取审计日志",
+ "ready": "就绪",
+ "Replace media bytes": "替换媒体文件",
+ "Resize {0} panel": "调整{0}面板大小",
+ "Retry": "重试",
+ "Route": "路由",
+ "Run the migration SSE that moves bytes between storage adapters after an election change.": "存储选择变更后,运行迁移 SSE,在存储适配器之间移动文件。",
+ "Scoped to {0}": "作用域:{0}",
+ "See and open the four built-in system tables (Posts, Pages, Components, Layouts) in the Data workspace.": "在数据工作区中查看并打开四个内置系统表(文章、页面、组件、布局)。",
+ "See the installed plugin list, masked settings, schedules, and event stream.": "查看已安装插件列表、脱敏设置、计划任务和事件流。",
+ "Seed your design tokens from the Core Framework defaults — colors, a fluid type scale, a spacing scale, and their utility classes. Pick the state you want; switching adds what's missing and strips what the new state drops.": "使用核心框架默认值初始化设计 Token,包括颜色、流式字号、间距比例及其实用类。选择需要的状态;切换时会补齐缺失项并移除新状态不再包含的内容。",
+ "Select all": "全选",
+ "Select all {0} capabilities": "选择全部 {0} 项权限",
+ "Select all capabilities": "选择所有权限",
+ "Select at least one post type.": "请至少选择一种文章类型。",
+ "selected": "已选择",
+ "Sign out": "退出登录",
+ "Sign out all devices": "退出所有设备",
+ "Signing out other devices…": "正在退出其他设备…",
+ "Signing out…": "正在退出…",
+ "Site": "站点",
+ "Site name": "站点名称",
+ "Site-wide author-facing fields.": "面向作者的全站字段。",
+ "Slug": "别名",
+ "Soft-delete assets to trash; hard-purge (also requires step-up) removes the bytes from disk.": "将资源软删除到回收站;彻底清除还需二次验证,并会从磁盘移除文件。",
+ "still assigned to": "仍分配给",
+ "Template settings": "模板设置",
+ "The homepage is always served at “/”.": "首页始终通过“/”提供。",
+ "theme": "主题",
+ "This action requires a recent password re-entry. You'll stay signed in here.": "此操作需要重新输入密码;当前设备仍会保持登录。",
+ "This action requires your password and a current authentication code.": "此操作需要输入密码和当前验证码。",
+ "Title": "标题",
+ "Tool failed.": "工具执行失败。",
+ "Tree preview": "树形预览",
+ "Upload a JSON bundle to merge or replace local data. `replace` mode additionally requires Manage content and step-up.": "上传 JSON 数据包以合并或替换本地数据。“替换”模式还需要“管理内容”权限和二次验证。",
+ "Upload and edit media": "上传和编辑媒体",
+ "Upload assets, edit metadata (alt text, caption, tags), manage folders, restore from trash.": "上传资源、编辑元数据(替代文本、说明、标签)、管理文件夹以及从回收站恢复。",
+ "URL slug": "URL 别名",
+ "Use AI chat": "使用 AI 对话",
+ "Users": "用户",
+ "Uses of .{0}": ".{0} 的使用位置",
+ "Variables only": "仅变量",
+ "View dashboard": "查看仪表盘",
+ "View site": "查看站点",
+ "View site-wide AI usage, cost, and error events across all users.": "查看所有用户的全站 AI 用量、费用和错误事件。",
+ "View the audit log and the Dashboard activity widget.": "查看审计日志和仪表盘活动组件。",
+ "will be deleted from the site.": "将从站点中删除。",
+ "Wrap in a Loop or open a postType template to bind to row fields.": "将内容包装进循环,或打开文章类型模板以绑定数据行字段。",
+ "Your role does not include access to this admin section.": "你的角色无权访问此后台区域。",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/site-a.ts b/src/admin/i18n/locales/zh-CN/site-a.ts
new file mode 100644
index 000000000..d5c0c11a2
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/site-a.ts
@@ -0,0 +1,302 @@
+export const siteAZhCN = {
+ "— Choose a table —": "— 选择数据表 —",
+ "— Choose an array field —": "— 选择数组字段 —",
+ "· {0} subset{1}": "· {0} 个子集",
+ "” is already installed.": "”已经安装。",
+ "(min-width: 768px)": "(最小宽度:768px)",
+ "(missing)": "(缺失)",
+ "(orientation: landscape)": "(方向:横向)",
+ ") references will be rewritten to this variable.": ")的引用将改写为此变量。",
+ "{0} · detected {1}": "{0} · 检测到 {1}",
+ "{0} (custom)": "{0}(自定义)",
+ "{0} {1} will be removed from every element that uses them. This can be undone with Ctrl/Cmd+Z.": "将从所有使用位置移除 {0} 个{1}。可使用 Ctrl/Cmd+Z 撤销。",
+ "{0} and any of its children will be removed. This can be undone with Ctrl/Cmd+Z.": "{0}及其所有子项将被移除。可使用 Ctrl/Cmd+Z 撤销。",
+ "{0} binding removed": "已移除{0}绑定",
+ "{0} bound": "已绑定 {0}",
+ "{0} color tokens": "{0} 个颜色 Token",
+ "{0} complete": "{0} 完成",
+ "{0} custom CSS": "{0}自定义 CSS",
+ "{0} is not compatible with this control type.": "{0}与此控件类型不兼容。",
+ "{0} is preparing": "{0}正在准备",
+ "{0} items": "{0} 项",
+ "{0} layers (and their children) will be removed. This can be undone with Ctrl/Cmd+Z.": "将移除 {0} 个图层及其子图层。可使用 Ctrl/Cmd+Z 撤销。",
+ "{0} layers and any children will be removed. This can be undone with Ctrl/Cmd+Z.": "将移除 {0} 个图层及其所有子图层。可使用 Ctrl/Cmd+Z 撤销。",
+ "{0} mode": "{0}模式",
+ "{0} module is disabled.": "{0}模块已禁用。",
+ "{0} needs {1} packages": "{0}需要 {1} 个依赖包",
+ "{0} needs 1 package": "{0}需要 1 个依赖包",
+ "{0} of {1} packages shown": "显示 {0}/{1} 个依赖包",
+ "{0} param binding": "{0}参数绑定",
+ "{0} preview": "{0}预览",
+ "{0} preview {1} {2}": "{0}预览 {1} {2}",
+ "{0} preview: {1}": "{0}预览:{1}",
+ "{0} previews": "{0} 个预览",
+ "{0} running": "{0}正在运行",
+ "{0} sandbox preview": "{0}沙箱预览",
+ "{0} scope": "{0}作用域",
+ "{0} settings": "{0}设置",
+ "{0} source": "{0}来源",
+ "{0} suggestions": "{0} 条建议",
+ "{0} target pages": "{0} 个目标页面",
+ "{0} target templates": "{0} 个目标模板",
+ "{0} updated": "{0} 已更新",
+ "{0} value": "{0}值",
+ "{0} variants": "{0} 个变体",
+ "{0}, {1}px mobile, {2}px desktop": "{0},移动端 {1}px,桌面端 {2}px",
+ "{0}: desktop {1}px{2}": "{0}:桌面端 {1}px{2}",
+ "{0}: mobile {1}px{2}": "{0}:移动端 {1}px{2}",
+ "{0}% available": "剩余 {0}%",
+ "{0}px viewport": "{0}px 视口",
+ "@media (prefers-reduced-motion: reduce) {\n [data-testid=\"canvas-transform-layer\"] {\n transition: none !important;\n }\n }": "@media (prefers-reduced-motion: reduce) {\n [data-testid=\"canvas-transform-layer\"] {\n transition: none !important;\n }\n }",
+ "“": "“",
+ "”": "”",
+ "#000000 or rgb(...)": "#000000 或 rgb(...) ",
+ "+ Create": "+ 创建",
+ "0px": "0px",
+ "A change was reverted": "一项更改已还原",
+ "A font named “": "名为“",
+ "A message can contain up to {0} images. {1} {2} not attached.": "每条消息最多可包含 {0} 张图片。{1} 张{2}未附加。",
+ "Aa": "Aa",
+ "Absolute": "绝对定位",
+ "add": "添加",
+ "Add {0} field": "添加{0}字段",
+ "Add a class to start styling this element": "添加类以开始设置此元素的样式",
+ "Add a provider credential, then choose a default model before starting a chat.": "添加提供商凭据并选择默认模型后,即可开始对话。",
+ "Add AI credentials to start chatting": "添加 AI 凭据以开始对话",
+ "Add attribute": "添加属性",
+ "Add class": "添加类",
+ "Add context…": "添加上下文…",
+ "Add custom font": "添加自定义字体",
+ "Add dependency": "添加依赖",
+ "Add font — {0}": "添加字体——{0}",
+ "Add Google font": "添加 Google 字体",
+ "Add named form fields before creating a CMS data table.": "创建 CMS 数据表前,请先添加带名称的表单字段。",
+ "Add option…": "添加选项…",
+ "Add or create a CSS selector": "添加或创建 CSS 选择器",
+ "Add or create selector…": "添加或创建选择器…",
+ "Add package name": "添加依赖包名称",
+ "Add param": "添加参数",
+ "Add property": "添加属性",
+ "Add step after": "在后面添加步骤",
+ "Add step before": "在前面添加步骤",
+ "Add SVG": "添加 SVG",
+ "Add to canvas": "添加到画布",
+ "Admins editing this site": "正在编辑此站点的管理员",
+ "Advanced": "高级",
+ "AI Assistant": "AI 助手",
+ "AI context remaining: {0}": "AI 剩余上下文:{0}",
+ "Align": "对齐",
+ "Align baseline": "基线对齐",
+ "Align end": "末端对齐",
+ "Align items": "项目对齐",
+ "Align start": "起始端对齐",
+ "Align stretch": "拉伸对齐",
+ "align-items: baseline": "align-items: baseline",
+ "align-items: center": "align-items: center",
+ "align-items: flex-end": "align-items: flex-end",
+ "align-items: flex-start": "align-items: flex-start",
+ "align-items: stretch": "align-items: stretch",
+ "All classes": "所有类",
+ "All tokens": "所有 Token",
+ "Alt color": "替代颜色",
+ "Alternate color swatch {0}": "替代颜色样本 {0}",
+ "Ambient selectors": "环境选择器",
+ "Animate scrolling when the tree jumps to a newly selected layer. Turn off for instant snapping.": "树跳转到新选中的图层时使用滚动动画。关闭后会立即跳转。",
+ "Animate the properties panel when switching between Style / Module / Component tabs.": "在样式、模块和组件标签页之间切换时,为属性面板使用动画。",
+ "Anonymously count which commands you run most often so the palette can surface them higher. Data never leaves this device.": "匿名统计最常用命令,以便命令面板将其优先显示。数据不会离开此设备。",
+ "Apply selected selectors to the selected element": "将所选选择器应用到所选元素",
+ "Apply to selected element": "应用到所选元素",
+ "Ask before removing a layer via the Delete key or context menu. Off by default to match power-user flow.": "使用 Delete 键或上下文菜单移除图层前先询问。默认关闭,以符合高效操作流程。",
+ "Aspect ratio": "宽高比",
+ "Assigned font": "已分配字体",
+ "Assistant": "助手",
+ "Assistant image": "助手图片",
+ "at": "在",
+ "Attached image: {0}": "已附加图片:{0}",
+ "Attached images": "已附加图片",
+ "Attribute name": "属性名称",
+ "Attributes": "属性",
+ "Auto-expand on selection": "选择时自动展开",
+ "available": "可用",
+ "Back to page": "返回页面",
+ "Backdrop filter": "背景滤镜",
+ "Background": "背景",
+ "Background image from media library": "来自媒体库的背景图片",
+ "Background utility": "背景实用类",
+ "Base": "基础",
+ "Base scale index": "基础比例索引",
+ "Base step": "基础步长",
+ "Binary file — no preview available": "二进制文件——无法预览",
+ "Bind {0}": "绑定{0}",
+ "Bind this control to a table field.": "将此控件绑定到数据表字段。",
+ "Bind to data field": "绑定到数据字段",
+ "Bind to param": "绑定到参数",
+ "Black 900": "黑色 900",
+ "Bold 700": "粗体 700",
+ "Border": "边框",
+ "Border {0} style": "{0}边框样式",
+ "Border {0} width": "{0}边框宽度",
+ "Border radius {0}": "{0}圆角半径",
+ "Border radius corner": "边框圆角",
+ "Border side": "边框侧边",
+ "Border utility": "边框实用类",
+ "Both a property name and a value are required.": "属性名称和值均为必填项。",
+ "Building desktop preview…": "正在构建桌面端预览…",
+ "Building preview…": "正在构建预览…",
+ "Cache read": "缓存读取",
+ "Cache write": "缓存写入",
+ "Calculating size…": "正在计算大小…",
+ "Cancel remove": "取消移除",
+ "Canvas": "画布",
+ "Canvas — infinite editing surface": "画布——无限编辑区域",
+ "Canvas frame for {0}": "{0}的画布框架",
+ "Canvas mode": "画布模式",
+ "Canvas navigation": "画布导航",
+ "Canvas view": "画布视图",
+ "categories": "分类",
+ "Category": "分类",
+ "Change {0} value": "更改{0}值",
+ "Change not applied": "更改未应用",
+ "Chat history": "对话历史",
+ "Checking whether this model accepts images…": "正在检查此模型是否支持图片…",
+ "Choose a model": "选择模型",
+ "Choose a model below to start": "在下方选择模型以开始",
+ "Choose a model below, or set a default in AI settings.": "在下方选择模型,或在 AI 设置中指定默认模型。",
+ "Choose a model to get started": "选择模型以开始",
+ "Choose a target data table before publishing this CMS-native form.": "发布此 CMS 原生表单前,请选择目标数据表。",
+ "Choose a vision-capable model or remove the image.": "请选择支持视觉的模型,或移除图片。",
+ "Choose an agent-capable model that supports tool calling.": "请选择支持工具调用的智能体模型。",
+ "Choose field": "选择字段",
+ "Choose preset ratio": "选择预设比例",
+ "Choose table": "选择数据表",
+ "Choose whether the admin interface uses the default dark chrome or a light theme.": "选择后台界面使用默认深色外观还是浅色主题。",
+ "Circular component reference": "组件循环引用",
+ "Class generator": "类生成器",
+ "Class name": "类名",
+ "Class pattern": "类命名模式",
+ "Clear {0} ({1})": "清除{0}({1})",
+ "Clear border": "清除边框",
+ "Clear radius": "清除圆角",
+ "Click any file in the Files panel to open it here.": "点击文件面板中的任意文件,即可在这里打开。",
+ "Click here to create your first color": "点击这里创建第一个颜色",
+ "Click here to create your spacing scale": "点击这里创建间距比例",
+ "Close advanced options": "关闭高级选项",
+ "Close preview": "关闭预览",
+ "CMS-native": "CMS 原生",
+ "Code": "代码",
+ "Code Editor": "代码编辑器",
+ "Collaboration startup failed": "协作功能启动失败",
+ "Collapse {0} frame": "折叠{0}框架",
+ "Color": "颜色",
+ "Colors": "颜色",
+ "column-gap": "column-gap",
+ "Columns": "列",
+ "Comfortable": "舒适",
+ "Compact": "紧凑",
+ "Compact packs more on screen; comfortable gives larger touch targets and more breathing room.": "紧凑模式可在屏幕显示更多内容;舒适模式提供更大的点击区域和更宽松的间距。",
+ "Component parameters": "组件参数",
+ "Component params": "组件参数",
+ "Componentize": "转为组件",
+ "Components": "组件",
+ "Condition": "条件",
+ "Confirm before deleting layers": "删除图层前确认",
+ "Confirm remove {0}": "确认移除{0}",
+ "Connect an AI provider": "连接 AI 提供商",
+ "connected": "已连接",
+ "connecting": "正在连接",
+ "Connecting": "正在连接",
+ "Container": "容器",
+ "Container name (CSS, optional)": "容器名称(CSS,可选)",
+ "Container query": "容器查询",
+ "Container width": "容器宽度",
+ "Context remaining": "剩余上下文",
+ "Context type": "上下文类型",
+ "Control what is shown next to each row in the DOM tree.": "控制 DOM 树中每行旁边显示的内容。",
+ "Conversation": "对话",
+ "Conversation billing": "对话费用",
+ "Conversation history": "对话历史",
+ "Convert to page": "转换为页面",
+ "Copy": "复制",
+ "Copy {0}": "复制{0}",
+ "Copy image": "复制图片",
+ "Copy selector": "复制选择器",
+ "Could not delete font files": "无法删除字体文件",
+ "Could not estimate size": "无法估算大小",
+ "Could not verify image support for this model. Choose another model or remove the image.": "无法确认此模型是否支持图片。请选择其他模型或移除图片。",
+ "Couldn't attach image": "无法附加图片",
+ "Couldn't build preview": "无法构建预览",
+ "Couldn't change model": "无法更换模型",
+ "Couldn't copy image": "无法复制图片",
+ "Couldn't delete conversation": "无法删除对话",
+ "Couldn’t estimate size": "无法估算大小",
+ "Couldn't load conversations": "无法加载对话",
+ "Couldn’t load that SVG file.": "无法加载该 SVG 文件。",
+ "Couldn’t read a valid SVG from that file.": "无法从该文件读取有效的 SVG。",
+ "Couldn't save image": "无法保存图片",
+ "Couldn't save to Media": "无法保存到媒体库",
+ "Couldn't send message": "无法发送消息",
+ "Create": "创建",
+ "Create color": "创建颜色",
+ "Create font token": "创建字体 Token",
+ "Create param": "创建参数",
+ "Create selector": "创建选择器",
+ "Create table": "创建数据表",
+ "Create target table": "创建目标表",
+ "Create token": "创建 Token",
+ "CSS media query": "CSS 媒体查询",
+ "CSS property": "CSS 属性",
+ "Current model · per 1M tokens": "当前模型 · 每百万 Token",
+ "Current zoom {0}%. Click to reset to 100%.": "当前缩放 {0}%。点击重置为 100%。",
+ "Custom action": "自定义操作",
+ "Custom CSS background (gradient, image-set, …)": "自定义 CSS 背景(渐变、image-set 等)",
+ "Custom font": "自定义字体",
+ "Custom font install failed": "自定义字体安装失败",
+ "Custom properties": "自定义属性",
+ "Custom URL": "自定义 URL",
+ "Cut": "剪切",
+ "Dark": "深色",
+ "Dark mode": "深色模式",
+ "Decrease {0} variants": "减少{0}变体",
+ "Default": "默认",
+ "Default color": "默认颜色",
+ "Default color swatch {0}": "默认颜色样本 {0}",
+ "Default viewport": "默认视口",
+ "Delete {0} breakpoint": "删除{0}断点",
+ "Delete {0} condition": "删除{0}条件",
+ "Delete breakpoint": "删除断点",
+ "Delete chat \"{0}\"": "删除对话“{0}”",
+ "Delete class": "删除类",
+ "Delete condition": "删除条件",
+ "Delete layer?": "删除图层?",
+ "Delete layers?": "删除多个图层?",
+ "Delete page": "删除页面",
+ "Delete page?": "删除页面?",
+ "Delete selected layers": "删除所选图层",
+ "Delete selected selectors": "删除所选选择器",
+ "Delete selector": "删除选择器",
+ "Delete selector {0}": "删除选择器 {0}",
+ "Delete selectors?": "删除多个选择器?",
+ "Delete spacing scale": "删除间距比例",
+ "Delete token \"{0}\"": "删除 Token“{0}”",
+ "Deleted items": "已删除项目",
+ "Deleted layout \"{0}\"": "已删除布局“{0}”",
+ "dependencies": "依赖",
+ "Dependencies": "依赖",
+ "Dependency resolution failed": "依赖解析失败",
+ "Describe what you want to build and I'll do it for you.": "描述你想构建的内容,我会帮你完成。",
+ "Deselect all": "全部取消选择",
+ "Design": "设计",
+ "Desktop": "桌面端",
+ "Desktop preview of {0}": "{0}的桌面端预览",
+ "devDependencies": "开发依赖",
+ "devDependency": "开发依赖",
+ "Dim inactive viewports when editing": "编辑时淡化非活动视口",
+ "Direction": "方向",
+ "Disable spacing framework": "禁用间距框架",
+ "Disable typography framework": "禁用排版框架",
+ "Display": "显示",
+ "Display a tinted pill with the underlying HTML tag (div, header, img, …) before each layer name.": "在每个图层名称前显示带色胶囊,标注底层 HTML 标签(div、header、img 等)。",
+ "Display assigned CSS class names after each layer name in CSS-selector form (e.g. `.header.padding-m`).": "在每个图层名称后以 CSS 选择器形式显示已分配的类名(例如 `.header.padding-m`)。",
+ "Display the module type icon (Container, Text, Image, …) at the start of each layer row.": "在每个图层行开头显示模块类型图标(容器、文本、图片等)。",
+ "display: flex": "display: flex",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/site-b.ts b/src/admin/i18n/locales/zh-CN/site-b.ts
new file mode 100644
index 000000000..61b0c6561
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/site-b.ts
@@ -0,0 +1,302 @@
+export const siteBZhCN = {
+ "display: grid": "display: grid",
+ "DOM tree panel": "DOM 树面板",
+ "Draft creation failed": "草稿创建失败",
+ "Draft not saved yet": "草稿尚未保存",
+ "Draft synced": "草稿已同步",
+ "drag": "拖动",
+ "Drag onto canvas to use this asset.": "拖到画布上以使用此资源。",
+ "Drag selected layers": "拖动所选图层",
+ "Drop {0}": "放下{0}",
+ "Dropped on canvas.": "已放到画布上。",
+ "Duplicate selected layers": "复制所选图层",
+ "Duplicate selected selectors": "复制所选选择器",
+ "e.g. Acme Grotesk": "例如:Acme Grotesk",
+ "e.g. Dark mode": "例如:深色模式",
+ "Edit {0} border": "编辑{0}边框",
+ "Edit {0} breakpoint": "编辑{0}断点",
+ "Edit {0} condition": "编辑{0}条件",
+ "Edit {0} corner": "编辑{0}圆角",
+ "Edit breakpoint": "编辑断点",
+ "Edit code": "编辑代码",
+ "Edit color {0}": "编辑颜色 {0}",
+ "Edit condition": "编辑条件",
+ "Edit custom font — {0}": "编辑自定义字体——{0}",
+ "Edit font — {0}": "编辑字体——{0}",
+ "Edit font token": "编辑字体 Token",
+ "Edit on canvas": "在画布中编辑",
+ "Edit selector {0}": "编辑选择器 {0}",
+ "Edit styles": "编辑样式",
+ "Edit SVG": "编辑 SVG",
+ "Edit track template": "编辑轨道模板",
+ "Editing all corners": "正在编辑所有圆角",
+ "Editing all sides": "正在编辑所有边",
+ "Editing component": "正在编辑组件",
+ "Editing context": "编辑上下文",
+ "Editing context: {0}": "编辑上下文:{0}",
+ "Editing one corner": "正在编辑单个圆角",
+ "Editing one side": "正在编辑单边",
+ "Editing template": "正在编辑模板",
+ "Editor toolbar": "编辑器工具栏",
+ "Effects": "效果",
+ "Element name": "元素名称",
+ "Element options": "元素选项",
+ "em": "em",
+ "Enabled": "已启用",
+ "Enter a width in pixels.": "请输入以像素为单位的宽度。",
+ "Enter custom ratio": "输入自定义比例",
+ "Environment": "环境",
+ "Environment media query": "环境媒体查询",
+ "Existing var(--": "现有 var(--",
+ "Expand style sections by default": "默认展开样式分区",
+ "Expand the ancestors of the selected layer so it stays visible in the tree.": "展开所选图层的所有上级,使其在树中保持可见。",
+ "Explorer": "资源管理器",
+ "Expose {0} as param": "将{0}公开为参数",
+ "Expose as component param": "公开为组件参数",
+ "Extra large": "超大",
+ "ExtraBold 800": "特粗体 800",
+ "ExtraLight 200": "特细体 200",
+ "Failed to convert: {0}": "转换失败:{0}",
+ "Failed to create target data table.": "创建目标数据表失败。",
+ "Failed to load CMS site": "加载 CMS 站点失败",
+ "Failed to load Google fonts list": "加载 Google 字体列表失败",
+ "Failed to load media": "加载媒体失败",
+ "Failed to load module inserter preferences": "加载模块插入器偏好失败",
+ "Failed to save module inserter preferences": "保存模块插入器偏好失败",
+ "Failed to start collaborative editing": "启动协作编辑失败",
+ "Failed to update conversation provider.": "更新对话提供商失败。",
+ "Fallback": "回退值",
+ "Fallback only": "仅回退值",
+ "Feature query": "特性查询",
+ "Field": "字段",
+ "Fields will be inferred from the controls in this form.": "字段会根据此表单中的控件自动推断。",
+ "Fill utility": "填充实用类",
+ "Find it under Layouts in the module inserter.": "可在模块插入器的“布局”中找到。",
+ "Flex": "弹性布局",
+ "Flex direction": "弹性方向",
+ "Flex layout": "弹性布局",
+ "Flex wrap": "弹性换行",
+ "Flexbox": "Flexbox",
+ "Font family name": "字体族名称",
+ "Font files from media (": "来自媒体库的字体文件(",
+ "Font files in media library": "媒体库中的字体文件",
+ "Font install failed": "字体安装失败",
+ "Font still in use": "字体仍在使用",
+ "Font tokens": "字体 Token",
+ "Font upload failed": "字体上传失败",
+ "Font variable": "字体变量",
+ "font-size": "font-size",
+ "Form ID": "表单 ID",
+ "Form mode": "表单模式",
+ "Form settings updated": "表单设置已更新",
+ "Frame width (px)": "框架宽度(px)",
+ "Frame width: {0} ({1}px)": "框架宽度:{0}({1}px)",
+ "Framework": "框架",
+ "Frequent": "常用",
+ "From library": "来自媒体库",
+ "gap": "间距",
+ "Gap": "间距",
+ "Generate shades": "生成暗色阶",
+ "Generate tints": "生成浅色阶",
+ "Generate utility": "生成实用类",
+ "Generate utility classes": "生成实用类",
+ "Generated utility": "已生成的实用类",
+ "Google": "Google",
+ "Google fonts": "Google 字体",
+ "Grid": "网格",
+ "Grid layout": "网格布局",
+ "Handwriting": "手写体",
+ "Hidden": "已隐藏",
+ "Home": "首页",
+ "How the canvas renders and reacts as you edit.": "控制画布在编辑时的渲染和交互方式。",
+ "HTML attributes": "HTML 属性",
+ "i": "i",
+ "Icon": "图标",
+ "id, aria-label, data-name": "id、aria-label、data-name",
+ "Image copied": "图片已复制",
+ "Image copying is not supported by this browser": "此浏览器不支持复制图片",
+ "Image is not loaded or has no natural dimensions.": "图片尚未加载或没有原始尺寸。",
+ "Image preview": "图片预览",
+ "Image preview: {0}": "图片预览:{0}",
+ "Image too large": "图片过大",
+ "Images are still processing": "图片仍在处理中",
+ "Images captured by assistant tools": "助手工具捕获的图片",
+ "Images from assistant": "来自助手的图片",
+ "Images from you": "来自你的图片",
+ "in ·": "输入 ·",
+ "in use": "使用中",
+ "Increase {0} variants": "增加{0}变体",
+ "Inherit": "继承",
+ "Inline": "行内",
+ "Input": "输入",
+ "Insert binding for {0}": "插入{0}的绑定",
+ "Insert module": "插入模块",
+ "Insert module here": "在此插入模块",
+ "Insert modules": "插入模块",
+ "Inserted {0}": "已插入{0}",
+ "Inserted at the current selection.": "已插入到当前选择位置。",
+ "install failed": "安装失败",
+ "Installed fonts": "已安装字体",
+ "Interaction": "交互",
+ "is not currently registered.": "当前未注册。",
+ "Italic": "斜体",
+ "Italic for {0}": "{0}的斜体",
+ "Justify center": "居中分布",
+ "Justify content": "内容分布",
+ "Justify end": "末端分布",
+ "Justify items": "项目分布",
+ "Justify start": "起始端分布",
+ "justify-content: center": "justify-content: center",
+ "justify-content: flex-end": "justify-content: flex-end",
+ "justify-content: flex-start": "justify-content: flex-start",
+ "justify-content: space-around": "justify-content: space-around",
+ "justify-content: space-between": "justify-content: space-between",
+ "Landscape": "横向",
+ "Laptop": "笔记本电脑",
+ "Large": "大",
+ "layers": "个图层",
+ "Layers": "图层",
+ "Layers panel": "图层面板",
+ "layers selected": "个图层已选择",
+ "Layout": "布局",
+ "Layout name": "布局名称",
+ "letter-spacing": "letter-spacing",
+ "Light": "浅色",
+ "Light 300": "细体 300",
+ "Light mode": "浅色模式",
+ "line-height": "line-height",
+ "linear-gradient(135deg, #f9fafb, #e5e7eb)": "linear-gradient(135deg, #f9fafb, #e5e7eb)",
+ "Link all {0} sides": "关联全部 {0} 边",
+ "Link all corners": "关联所有圆角",
+ "Link all sides": "关联所有边",
+ "Link type": "链接类型",
+ "Linked — edits all four sides": "已关联——同时编辑四边",
+ "Live frame width": "实时框架宽度",
+ "Loading code editor…": "正在加载代码编辑器…",
+ "Loading selectors": "正在加载选择器",
+ "Loading tables...": "正在加载数据表…",
+ "Loading target table": "正在加载目标表",
+ "Locked": "已锁定",
+ "Locked at {0}": "锁定于 {0}",
+ "Locked utility selectors can’t be deleted": "锁定的实用类选择器不能删除",
+ "Locked utility selectors can’t be duplicated": "锁定的实用类选择器不能复制",
+ "Loop": "循环",
+ "Manage framework": "管理框架",
+ "margin": "外边距",
+ "Margin": "外边距",
+ "margin-bottom": "下外边距",
+ "margin-left": "左外边距",
+ "margin-right": "右外边距",
+ "margin-top": "上外边距",
+ "max": "最大",
+ "Max {0}": "最大{0}",
+ "Max ratio": "最大比例",
+ "Max-width": "最大宽度",
+ "Maximum width": "最大宽度",
+ "Media query": "媒体查询",
+ "Medium 500": "中等 500",
+ "Message image limit reached": "已达到消息图片上限",
+ "Message to AI assistant": "发送给 AI 助手的消息",
+ "min": "最小",
+ "Min {0}": "最小{0}",
+ "Min ratio": "最小比例",
+ "Min-width": "最小宽度",
+ "min-width: 400px": "min-width: 400px",
+ "Minimum width": "最小宽度",
+ "Missing fields": "缺少字段",
+ "Missing sandbox runtime for": "缺少沙箱运行时:",
+ "Mobile": "移动端",
+ "Mode": "模式",
+ "Module categories": "模块分类",
+ "Module inserter keyboard shortcuts": "模块插入器键盘快捷键",
+ "Module inserter view": "模块插入器视图",
+ "Module settings — {0}": "模块设置——{0}",
+ "Monitor": "显示器",
+ "Mono": "等宽字体",
+ "Mouse / hover": "鼠标/悬停",
+ "Move": "移动",
+ "Move left": "左移",
+ "Move right": "右移",
+ "Multi-select actions": "多选操作",
+ "Multi-select selector actions": "多选选择器操作",
+ "navigate": "导航",
+ "New {0} folder": "新建{0}文件夹",
+ "New chat": "新建对话",
+ "New param name": "新参数名称",
+ "New property name": "新属性名称",
+ "New property value": "新属性值",
+ "No {0} scales yet.": "暂无{0}比例。",
+ "No AI provider credentials are configured yet.": "尚未配置 AI 提供商凭据。",
+ "No attributes set": "未设置属性",
+ "No chats yet.": "暂无对话。",
+ "No colors match the current filters.": "没有颜色符合当前筛选条件。",
+ "No colors yet.": "暂无颜色。",
+ "No compatible fields in the target table.": "目标表中没有兼容字段。",
+ "No dependencies yet.": "暂无依赖。",
+ "No elements match": "没有匹配的元素",
+ "No files selected": "未选择文件",
+ "No font files in your media library yet. Upload a .woff2, .woff, .ttf or .otf file to get started.": "媒体库中暂无字体文件。上传 .woff2、.woff、.ttf 或 .otf 文件即可开始。",
+ "No font tokens yet.": "暂无字体 Token。",
+ "No fonts installed yet.": "尚未安装字体。",
+ "No fonts match \"": "没有字体匹配“",
+ "No manual sizes yet.": "暂无手动尺寸。",
+ "No matching styles.": "没有匹配的样式。",
+ "No modules match": "没有匹配的模块",
+ "No packages matching \"{0}\"": "没有依赖包匹配“{0}”",
+ "No pages to target": "没有可作为目标的页面",
+ "No reusable selectors yet.": "暂无可复用选择器。",
+ "No selectors match “": "没有选择器匹配“",
+ "No SVG": "没有 SVG",
+ "No templates to target": "没有可作为目标的模板",
+ "Node content appears clipped by hidden overflow.": "节点内容似乎被隐藏溢出裁剪。",
+ "Node extends beyond the captured region.": "节点超出了捕获区域。",
+ "Node has text content but no visible layout box.": "节点包含文本,但没有可见布局框。",
+ "Node-scoped module style layer": "节点作用域的模块样式层",
+ "none": "无",
+ "Not a valid CSS property name.": "不是有效的 CSS 属性名称。",
+ "Not supported in this browser": "此浏览器不支持",
+ "Off": "关闭",
+ "offline": "离线",
+ "Offline — reconnecting": "离线——正在重新连接",
+ "Offset": "偏移",
+ "On": "开启",
+ "Only one content outlet": "只能有一个内容出口",
+ "Open {0} breakpoint in live mode": "在线上模式中打开{0}断点",
+ "Open {0} in live mode": "在线上模式中打开{0}",
+ "Open advanced options": "打开高级选项",
+ "Open AI settings": "打开 AI 设置",
+ "Open component {0}": "打开组件 {0}",
+ "Open component in canvas": "在画布中打开组件",
+ "Open every property section (Module, Layout, Typography, …) when an element is selected. Turn off to start with all sections collapsed.": "选择元素时展开所有属性分区(模块、布局、排版等)。关闭后所有分区默认折叠。",
+ "Open image preview: {0}": "打开图片预览:{0}",
+ "Open in canvas": "在画布中打开",
+ "Open it in canvas to add parameters.": "请在画布中打开以添加参数。",
+ "Open page {0}": "打开页面 {0}",
+ "Open settings": "打开设置",
+ "Open template {0}": "打开模板 {0}",
+ "Order by": "排序方式",
+ "out": "输出",
+ "Outline": "轮廓",
+ "Outline offset": "轮廓偏移",
+ "Output": "输出",
+ "Overridden": "已覆盖",
+ "package-name": "依赖包名称",
+ "padding": "内边距",
+ "padding-bottom": "下内边距",
+ "padding-left": "左内边距",
+ "padding-right": "右内边距",
+ "padding-top": "上内边距",
+ "Page preview": "页面预览",
+ "Panel": "面板",
+ "Panel dock": "面板停靠区",
+ "Param": "参数",
+ "Param name": "参数名称",
+ "Parameter name": "参数名称",
+ "paramName": "参数名称",
+ "Paste": "粘贴",
+ "Paste at anchor": "粘贴到锚点",
+ "Paste HTML here…": "在此粘贴 HTML…",
+ "Path changes": "路径更改",
+ "Pick a model below, or set a default in AI settings so it's ready every time you open this chat.": "在下方选择模型,或在 AI 设置中指定默认模型,使其在每次打开对话时自动就绪。",
+ "Placed {0}": "已放置{0}",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/site-c.ts b/src/admin/i18n/locales/zh-CN/site-c.ts
new file mode 100644
index 000000000..03e173cd3
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/site-c.ts
@@ -0,0 +1,303 @@
+export const siteCZhCN = {
+ "Placement": "位置",
+ "Plugin layout": "插件布局",
+ "Plugin panel": "插件面板",
+ "Portrait": "纵向",
+ "Position": "定位",
+ "Position absolute": "绝对定位",
+ "Position relative": "相对定位",
+ "position: absolute": "position: absolute",
+ "position: relative": "position: relative",
+ "Preparing…": "正在准备…",
+ "Presets": "预设",
+ "preview": "预览",
+ "Preview —": "预览——",
+ "Preview attached image: {0}": "预览已附加图片:{0}",
+ "Preview frame on canvas": "在画布上显示预览框架",
+ "Preview page": "预览页面",
+ "Preview source": "预览来源",
+ "Preview state": "预览状态",
+ "Preview suggestions on hover": "悬停时预览建议",
+ "Preview text": "预览文本",
+ "Preview unavailable": "预览不可用",
+ "Preview: {0}": "预览:{0}",
+ "Previewing": "正在预览",
+ "Print": "打印",
+ "Promote a property to create your first param.": "将属性提升为参数以创建第一个参数。",
+ "Properties": "属性",
+ "Properties editor": "属性编辑器",
+ "Properties panel": "属性面板",
+ "property": "属性",
+ "Publish failed": "发布失败",
+ "px": "px",
+ "Radius": "圆角",
+ "Re-resolve": "重新解析",
+ "Ready": "就绪",
+ "Reassign or delete the font tokens that reference this family before removing it.": "移除此字体族前,请重新分配或删除引用它的字体 Token。",
+ "Recent": "最近",
+ "Reconnecting": "正在重新连接",
+ "Redo ({0})": "重做({0})",
+ "Reduced motion": "减少动态效果",
+ "Refresh scripts": "刷新脚本",
+ "Regular 400": "常规 400",
+ "Relative": "相对定位",
+ "rem": "rem",
+ "Remove {0} from selection": "从选择中移除{0}",
+ "Remove {0} property": "移除{0}属性",
+ "Remove attached image: {0}": "移除已附加图片:{0}",
+ "Remove attribute": "移除属性",
+ "Remove binding for {0}": "移除{0}的绑定",
+ "Remove first step": "移除第一步",
+ "Remove from selected element": "从所选元素移除",
+ "Remove from selection": "从选择中移除",
+ "Remove from this element": "从此元素移除",
+ "Remove last step": "移除最后一步",
+ "Remove option {0}": "移除选项 {0}",
+ "Remove param": "移除参数",
+ "Remove scale": "移除比例",
+ "Rename {0}": "重命名{0}",
+ "Rename element": "重命名元素",
+ "Rename layout": "重命名布局",
+ "Rename selector": "重命名选择器",
+ "Rename selector {0}": "重命名选择器 {0}",
+ "Rename…": "重命名…",
+ "repeat(3, 1fr) · 200px 1fr · …": "repeat(3, 1fr) · 200px 1fr · …",
+ "Required by {0}": "{0}需要此依赖",
+ "Requires permission to upload media": "需要上传媒体权限",
+ "Reset {0} to default": "将{0}重置为默认值",
+ "Reset to 100% (Cmd/Ctrl+0)": "重置为 100%(Cmd/Ctrl+0)",
+ "Reset to default": "重置为默认值",
+ "Reset to defaults": "恢复默认值",
+ "Resize live frame from {0}": "从{0}调整实时框架大小",
+ "Resolving": "正在解析",
+ "Resolving dynamic content and page assets.": "正在解析动态内容和页面资源。",
+ "Resolving preview data…": "正在解析预览数据…",
+ "Retry resolve": "重试解析",
+ "Reusable selectors": "可复用选择器",
+ "row-gap": "row-gap",
+ "Run in canvas": "在画布中运行",
+ "Run scripts": "运行脚本",
+ "Runtime dependency issues": "运行时依赖问题",
+ "Sans": "无衬线字体",
+ "Save as layout": "保存为布局",
+ "Save as layout…": "保存为布局…",
+ "Save to desktop": "保存到桌面",
+ "Save to Media": "保存到媒体库",
+ "Saved layout": "已保存布局",
+ "Saved layout \"{0}\"": "已保存布局“{0}”",
+ "Saved Visual Component": "已保存的可视化组件",
+ "Saves as:": "保存为:",
+ "Saves this element and everything inside it — content, settings, and classes — for exact re-insertion from the module inserter.": "保存此元素及其所有内部内容、设置和类,以便从模块插入器完整重新插入。",
+ "Scale admin interface text independently from spacing density.": "独立于间距密度缩放后台界面文字。",
+ "Scale mode": "比例模式",
+ "Scale name": "比例名称",
+ "Scales": "比例",
+ "Schedule publish…": "计划发布…",
+ "Scope": "作用域",
+ "Screenshot capture failed.": "截取屏幕截图失败。",
+ "Script enabled": "已启用脚本",
+ "Script format": "脚本格式",
+ "Script imports": "脚本导入",
+ "Script placement": "脚本位置",
+ "Script priority": "脚本优先级",
+ "Script runtime settings": "脚本运行时设置",
+ "Script timing": "脚本时机",
+ "search": "搜索",
+ "Search class style properties to add": "搜索要添加的类样式属性",
+ "Search every module, layout & component...": "搜索所有模块、布局和组件…",
+ "Search Google Fonts": "搜索 Google 字体",
+ "Search Google Fonts…": "搜索 Google 字体…",
+ "Search layers": "搜索图层",
+ "Search layers…": "搜索图层…",
+ "Search modules": "搜索模块",
+ "Search modules…": "搜索模块…",
+ "Search packages": "搜索依赖包",
+ "Search packages...": "搜索依赖包…",
+ "Search styles in {0}...": "在{0}中搜索样式…",
+ "Select {0}": "选择{0}",
+ "Select a file to edit": "选择要编辑的文件",
+ "Select a page…": "选择页面…",
+ "Select an element on the canvas first": "请先在画布上选择元素",
+ "Select an element on the canvas to view its properties.": "在画布上选择元素以查看其属性。",
+ "Select canvas element from tree": "从树中选择画布元素",
+ "Select origin of {0}": "选择{0}的原点",
+ "Select parent or child layer": "选择父图层或子图层",
+ "Select parent or child layer for {0}": "为{0}选择父图层或子图层",
+ "Select selector {0}": "选择选择器 {0}",
+ "Selected layers (": "已选择图层(",
+ "Selected selectors (": "已选择选择器(",
+ "selected)": "已选择)",
+ "Selection actions": "选择操作",
+ "selector": "选择器",
+ "Selector": "选择器",
+ "selectors": "选择器",
+ "Selectors": "选择器",
+ "SemiBold 600": "半粗体 600",
+ "Send": "发送",
+ "Serif": "衬线字体",
+ "Set a default": "设置默认值",
+ "Set as homepage": "设为首页",
+ "Settings for the ⌘K command palette.": "⌘K 命令面板的设置。",
+ "Shade": "暗色阶",
+ "Show {0} frame": "显示{0}框架",
+ "Show {0} styles": "显示{0}样式",
+ "Show class names": "显示类名",
+ "Show HTML tag": "显示 HTML 标签",
+ "Show module icon": "显示模块图标",
+ "Show preview frame on canvas": "在画布上显示预览框架",
+ "sidebar": "侧边栏",
+ "Sides": "边",
+ "Site load failed": "站点加载失败",
+ "Site: {0}": "站点:{0}",
+ "Small": "小",
+ "Smartphone": "智能手机",
+ "Smooth scroll on tab change": "切换标签页时平滑滚动",
+ "Smooth scroll to selected": "平滑滚动到所选项",
+ "Source images must be smaller than {0} MB.": "源图片必须小于 {0} MB。",
+ "Space": "间距",
+ "Space around": "两侧均匀分布",
+ "Space between": "两端对齐分布",
+ "Spacing": "间距",
+ "Spacing scale chart": "间距比例图",
+ "Split — edit each side separately": "拆分——分别编辑每一边",
+ "Step labels": "步骤名称",
+ "Steps": "步骤",
+ "Stop": "停止",
+ "Stop exposing this property": "停止公开此属性",
+ "Style": "样式",
+ "Style categories": "样式分类",
+ "Style inline": "行内样式",
+ "Style just this element with an inline style attribute (no reusable class)": "仅使用行内 style 属性设置此元素样式(不创建可复用类)",
+ "Styles": "样式",
+ "Styles are read-only for your role": "你的角色只能查看样式",
+ "Stylesheet": "样式表",
+ "Stylesheet enabled": "已启用样式表",
+ "Stylesheet priority": "样式表优先级",
+ "Stylesheet settings": "样式表设置",
+ "Subgrid": "子网格",
+ "Submitting": "正在提交",
+ "Subsets (": "子集(",
+ "Success": "成功",
+ "Suggested": "建议",
+ "Supports": "支持",
+ "Switch document": "切换文档",
+ "Switch to {0} breakpoint": "切换到{0}断点",
+ "Sync failed": "同步失败",
+ "Table name": "数据表名称",
+ "Tablet": "平板电脑",
+ "Tell me what to build… (attach images or press Enter to send)": "告诉我想构建什么…(可附加图片,或按回车发送)",
+ "Temporarily apply class suggestions, design tokens (spacing, colour, …), and variable autocomplete entries to the selected canvas element while hovering them in the Properties panel.": "在属性面板中悬停类建议、设计 Token(间距、颜色等)和变量自动补全项时,将其临时应用到所选画布元素。",
+ "Text utility": "文本实用类",
+ "That file isn’t an SVG — pick a .svg file to inline.": "该文件不是 SVG;请选择 .svg 文件以内联。",
+ "That media query is not valid CSS.": "该媒体查询不是有效的 CSS。",
+ "That property has a dedicated control in another section.": "该属性在其他分区中有专用控件。",
+ "That query is not valid CSS.": "该查询不是有效的 CSS。",
+ "The bound field \"{0}\" no longer exists in {1}.": "已绑定字段“{0}”在{1}中已不存在。",
+ "The captured region has horizontal overflow.": "捕获区域存在水平溢出。",
+ "The captured region has vertical overflow.": "捕获区域存在垂直溢出。",
+ "The copied nodes include a content outlet and this document already has one.": "复制的节点包含内容出口,而此文档已经有一个。",
+ "The page body cannot be saved as a layout — save a section inside it instead.": "页面主体不能保存为布局;请改为保存其中的某个区域。",
+ "The pasted image could not be prepared.": "无法准备粘贴的图片。",
+ "Theme": "主题",
+ "Thin 100": "细体 100",
+ "This browser supports it": "此浏览器支持",
+ "This cannot be applied yet": "暂时无法应用",
+ "This component has no exposed parameters.": "此组件没有公开参数。",
+ "This control is not inside a form.": "此控件不在表单中。",
+ "This element can no longer be saved as a layout.": "此元素无法再保存为布局。",
+ "This image is already being saved to Media": "此图片已在保存到媒体库",
+ "This is a utility class. Utility classes have a single purpose and aren't meant to be edited.": "这是一个实用类。实用类用途单一,不应直接编辑。",
+ "This label has no form control after it.": "此标签后没有表单控件。",
+ "This layout contains a component that references the component being edited.": "此布局包含一个引用当前正在编辑组件的组件。",
+ "This layout includes a content outlet and this document already has one.": "此布局包含内容出口,而此文档已经有一个。",
+ "This selector is {0}.": "此选择器为{0}。",
+ "This submit button is not inside a form.": "此提交按钮不在表单中。",
+ "This will remove \"{0}\" from the site tree.": "这会从站点树中移除“{0}”。",
+ "Timing": "时机",
+ "Tint": "浅色阶",
+ "Tip: for viewport width, use the Viewport tab so the context gets a canvas frame.": "提示:如需设置视口宽度,请使用“视口”标签页,使上下文获得画布框架。",
+ "to": "到",
+ "to canvas": "到画布",
+ "Tokens": "Token",
+ "Tool result image": "工具结果图片",
+ "Touch": "触控",
+ "Track command usage (local only)": "记录命令使用情况(仅本地)",
+ "Transparent variants": "透明变体",
+ "Try a different search.": "请尝试其他搜索词。",
+ "Try: \"Add a hero section with a heading and button\"": "试试:“添加一个包含标题和按钮的主视觉区域”",
+ "Turn the sizes above into reusable utility classes you can apply across your site.": "将上方尺寸生成为可在全站应用的可复用实用类。",
+ "TV": "电视",
+ "Two controls inside this form use the name \"{0}\".": "此表单中有两个控件使用名称“{0}”。",
+ "Type to search or create a selector": "输入以搜索或创建选择器",
+ "Typography": "排版",
+ "Typography lives here": "排版设置位于这里",
+ "Typography scale preview": "排版比例预览",
+ "UI density": "界面密度",
+ "UI text size": "界面文字大小",
+ "Unable to delete media": "无法删除媒体",
+ "Unable to load media": "无法加载媒体",
+ "Unable to load media library": "无法加载媒体库",
+ "Unbind param": "解除参数绑定",
+ "Undo ({0})": "撤销({0})",
+ "Undo and redo": "撤销与重做",
+ "Undo selector {0} creation": "撤销创建选择器 {0}",
+ "Undo with Ctrl/Cmd+Z.": "可使用 Ctrl/Cmd+Z 撤销。",
+ "Undock": "取消停靠",
+ "Unknown component:": "未知组件:",
+ "Unknown module:": "未知模块:",
+ "Unknown module: {0}": "未知模块:{0}",
+ "Unlink {0} sides": "取消关联 {0} 边",
+ "Unlink corners": "取消关联圆角",
+ "Unlink sides": "取消关联边",
+ "Unsupported image": "不支持的图片",
+ "Untitled page": "未命名页面",
+ "Untitled template": "未命名模板",
+ "Unused": "未使用",
+ "Update spacing class generators": "更新间距类生成器",
+ "Update spacing scale": "更新间距比例",
+ "Update typography class generators": "更新排版类生成器",
+ "Updates after the next response": "将在下次回复后更新",
+ "Upload custom font": "上传自定义字体",
+ "Upload font file": "上传字体文件",
+ "Upload font files": "上传字体文件",
+ "Use a content outlet": "使用内容出口",
+ "Use a PNG, JPEG, or WebP image.": "请使用 PNG、JPEG 或 WebP 图片。",
+ "Use as template": "用作模板",
+ "Use in selected image": "用于所选图片",
+ "Use in selected video": "用于所选视频",
+ "used": "已使用",
+ "Used by": "使用位置",
+ "Utilities": "实用类",
+ "Utility": "实用类",
+ "Variable": "变量",
+ "Variable name": "变量名称",
+ "Variable prefix": "变量前缀",
+ "Variants (": "变体(",
+ "View utility": "查看实用类",
+ "Viewport": "视口",
+ "Viewport context": "视口上下文",
+ "Viewport CSS media query": "视口 CSS 媒体查询",
+ "Viewport frame width in pixels": "以像素表示的视口框架宽度",
+ "Viewport icon": "视口图标",
+ "Wait a moment, then send again.": "请稍候再重新发送。",
+ "Waiting for a response": "正在等待回复",
+ "Warnings": "警告",
+ "was added but does not match this element": "已添加,但与此元素不匹配",
+ "Weight for {0}": "{0}的字重",
+ "When a layer is selected and the properties panel is open, fade non-active viewport frames to focus attention on the one being edited.": "选择图层且属性面板打开时,淡化非活动视口框架,以突出当前编辑的视口。",
+ "Which viewport context the canvas focuses on when a site is opened. Mobile-first designers usually pick mobile.": "打开站点时画布聚焦的视口上下文。移动优先的设计通常选择移动端。",
+ "Width": "宽度",
+ "Width unit": "宽度单位",
+ "window": "窗口",
+ "Working…": "正在处理…",
+ "Wrap": "换行",
+ "Wrap in": "包裹到",
+ "Wrap in folder": "放入文件夹",
+ "Wrap selected layers": "包裹所选图层",
+ "Wrap…": "包裹…",
+ "You": "你",
+ "Your attachment": "你的附件",
+ "Your role can edit page copy but not classes or style overrides. Ask an editor to make visual changes.": "你的角色可以编辑页面文案,但不能修改类或样式覆盖。请联系编辑者进行视觉更改。",
+ "Zoom in (+)": "放大(+)",
+ "Zoom out (−)": "缩小(−)",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/site-d.ts b/src/admin/i18n/locales/zh-CN/site-d.ts
new file mode 100644
index 000000000..38496f052
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/site-d.ts
@@ -0,0 +1,203 @@
+export const siteDZhCN = {
+ "— Pick a source —": "— 选择数据源 —",
+ ".hero-card": ".hero-card",
+ ".hero-card, h1, .hero .title, a:hover": ".hero-card, h1, .hero .title, a:hover",
+ "“{0}” is already on this element": "“{0}”已在此元素上",
+ "{0} · {1}px": "{0} · {1}px",
+ "{0} (viewing another page)": "{0}(正在查看其他页面)",
+ "{0} {1} selected": "已选择 {0} 个{1}",
+ "{0} blocks · {1}": "{0} 个区块 · {1}",
+ "{0} choices": "{0} 个选项",
+ "{0} contexts": "{0} 个上下文",
+ "{0} favorite options": "{0} 个收藏选项",
+ "{0} Italic": "{0} 斜体",
+ "{0} layers selected": "已选择 {0} 个图层",
+ "{0} module": "{0} 模块",
+ "{0} of {1} context tokens available ({2}%)": "上下文令牌可用 {0}/{1}({2}%)",
+ "{0} panel ({1})": "{0}面板({1})",
+ "{0} params · Saved component": "{0} 个参数 · 已保存组件",
+ "{0} props": "{0} 个属性",
+ "{0} scale actions": "{0}比例尺操作",
+ "{0} template": "{0}模板",
+ "{0} tracks": "{0} 条轨道",
+ "{0} values": "{0} 个值",
+ "{0} variables": "{0} 个变量",
+ "1 block · {0}": "1 个区块 · {0}",
+ "1 context": "1 个上下文",
+ "1 param · Saved component": "1 个参数 · 已保存组件",
+ "1 prop": "1 个属性",
+ "Add {0} packages": "添加 {0} 个软件包",
+ "Add {0} scale": "添加{0}比例尺",
+ "Add {0} to notch favorites": "将{0}添加到顶部收藏",
+ "Add a class to unlock styles": "添加类以启用样式",
+ "Add class “{0}”": "添加类“{0}”",
+ "Add context": "添加上下文",
+ "Add scale": "添加比例尺",
+ "Add viewport": "添加视口",
+ "All pages": "所有页面",
+ "Ambient": "环境",
+ "Ascending (oldest first)": "升序(最早优先)",
+ "Attach images": "附加图片",
+ "auto": "自动",
+ "Body · --{0}": "正文 · --{0}",
+ "Body · {0}": "正文 · {0}",
+ "Body end": "正文末尾",
+ "Border {0} color": "{0}边框颜色",
+ "Bottom offset": "底部偏移",
+ "building": "正在构建",
+ "checking-model": "正在检查模型",
+ "Class actions": "类操作",
+ "Classic": "经典",
+ "Click to activate {0} breakpoint": "点击激活{0}断点",
+ "CMS-native form": "CMS 原生表单",
+ "Color token actions": "颜色令牌操作",
+ "column": "column",
+ "Column": "列",
+ "Column reverse": "反向列",
+ "column-reverse": "column-reverse",
+ "Connected to {0}.": "已连接到{0}。",
+ "Context has not been measured for this model yet; {0} token window": "尚未测量此模型的上下文;令牌窗口为 {0}",
+ "Create class “{0}”": "创建类“{0}”",
+ "Create scale": "创建比例尺",
+ "Create selector “{0}”": "创建选择器“{0}”",
+ "Creating...": "正在创建...",
+ "CSS selector": "CSS 选择器",
+ "current": "当前",
+ "Custom {0}": "自定义{0}",
+ "Custom action form": "自定义操作表单",
+ "Custom track template": "自定义轨道模板",
+ "declared as devDependency": "已声明为 devDependency",
+ "Delete {0} {1}": "删除{0}{1}",
+ "Delete {0} {1}?": "删除{0}{1}?",
+ "Dependency resolve failed — open the Dependencies panel to retry.": "依赖解析失败,请打开“依赖”面板重试。",
+ "Descending (newest first)": "降序(最新优先)",
+ "Design mode (multi-breakpoint canvas)": "设计模式(多断点画布)",
+ "Disable \"{0}\" {1} utility": "禁用“{0}”{1}实用类",
+ "Disable \"{0}\" shades": "禁用“{0}”深色阶",
+ "Disable \"{0}\" tints": "禁用“{0}”浅色阶",
+ "Disable \"{0}\" transparent steps": "禁用“{0}”透明度阶",
+ "Disable \"{0}\" utilities": "禁用“{0}”实用类",
+ "DOM ready": "DOM 就绪",
+ "Edit selector “{0}”": "编辑选择器“{0}”",
+ "Edit viewport": "编辑视口",
+ "first child": "第一个子元素",
+ "Form message": "表单消息",
+ "Getting full": "即将用满",
+ "Grid template columns": "网格模板列",
+ "Grid template rows": "网格模板行",
+ "h1, .hero .title, a:hover": "h1, .hero .title, a:hover",
+ "has an invalid package name": "的软件包名称无效",
+ "Head": "页头",
+ "Heading · --{0}": "标题 · --{0}",
+ "Heading · {0}": "标题 · {0}",
+ "Hide": "隐藏",
+ "Hide selected": "隐藏所选项",
+ "idle": "空闲",
+ "Idle": "空闲",
+ "Image actions": "图片操作",
+ "Immediate": "立即",
+ "Infinite scroll": "无限滚动",
+ "Inside {0}.": "位于{0}内。",
+ "Install font": "安装字体",
+ "Installing…": "正在安装…",
+ "Invalid": "无效",
+ "Invalid URL": "无效 URL",
+ "Left offset": "左侧偏移",
+ "left sidebar": "左侧边栏",
+ "Limit": "限制",
+ "Live mode (single real-size editable frame)": "实时模式(单个真实尺寸可编辑框架)",
+ "Loading Google Fonts": "正在加载 Google Fonts",
+ "Loading layers": "正在加载图层",
+ "Loading media library": "正在加载媒体库",
+ "Loading site": "正在加载站点",
+ "Manual": "手动",
+ "Max scale ratio": "最大缩放比例",
+ "Max screen width": "最大屏幕宽度",
+ "Max size": "最大尺寸",
+ "Maximum {0} images per message": "每条消息最多 {0} 张图片",
+ "Media Explorer": "媒体资源管理器",
+ "Min scale ratio": "最小缩放比例",
+ "Min screen width": "最小屏幕宽度",
+ "Min size": "最小尺寸",
+ "missing from dependencies": "依赖中缺失",
+ "model-error": "模型错误",
+ "Module": "模块",
+ "More {0} values": "更多{0}值",
+ "Nearly full": "接近用满",
+ "No AI provider configured for the \"{0}\" scope. Open /admin/ai/providers to add a credential, then /admin/ai/defaults to pick one.": "尚未为“{0}”范围配置 AI 提供商。请先打开 /admin/ai/providers 添加凭据,再前往 /admin/ai/defaults 选择提供商。",
+ "No selectors match “{0}”.": "没有与“{0}”匹配的选择器。",
+ "No selectors match the current filters.": "没有选择器符合当前筛选条件。",
+ "No unused selectors — every selector is in use.": "没有未使用的选择器,所有选择器均在使用中。",
+ "No user selectors yet.": "尚无用户选择器。",
+ "No utility selectors yet.": "尚无实用类选择器。",
+ "No wrap": "不换行",
+ "Node options": "节点选项",
+ "not available in browser runtime": "浏览器运行时不可用",
+ "Not measured": "尚未测量",
+ "nowrap": "nowrap",
+ "Open media {0}": "打开媒体{0}",
+ "Original order": "原始顺序",
+ "Padding": "内边距",
+ "Page element tree": "页面元素树",
+ "Page size": "每页数量",
+ "Pagination": "分页",
+ "parent": "父级",
+ "plugin": "插件",
+ "processing": "正在处理",
+ "Re-run scripts from current site state": "根据当前站点状态重新运行脚本",
+ "Remove {0} from notch favorites": "从顶部收藏中移除{0}",
+ "Rename token to \"{0}\"": "将令牌重命名为“{0}”",
+ "repeat({0}, 1fr)": "repeat({0}, 1fr)",
+ "Resize left sidebar": "调整左侧边栏大小",
+ "Resolving runtime packages…": "正在解析运行时软件包…",
+ "Retry preview": "重试预览",
+ "Rev": "反向",
+ "Reverse order": "反向顺序",
+ "Right offset": "右侧偏移",
+ "root": "根级",
+ "Row reverse": "反向行",
+ "row-reverse": "row-reverse",
+ "Run site scripts inside the editable frames": "在可编辑框架内运行站点脚本",
+ "Running": "正在运行",
+ "Save changes": "保存更改",
+ "Save layout": "保存布局",
+ "Save token": "保存令牌",
+ "Saved to Media": "已保存到媒体库",
+ "Search colors": "搜索颜色",
+ "Search selectors": "搜索选择器",
+ "Selector actions": "选择器操作",
+ "Selector does not match this element": "选择器与此元素不匹配",
+ "Selector suggestions": "选择器建议",
+ "Set a default in AI settings": "在 AI 设置中设为默认",
+ "Site item options": "站点项目选项",
+ "Specific pages": "指定页面",
+ "Specific templates": "指定模板",
+ "Submit button": "提交按钮",
+ "Submit selector": "提交按钮选择器",
+ "Targets {0}.": "目标为{0}。",
+ "Thanks. Your submission was received.": "谢谢,你的提交已收到。",
+ "The quick brown fox jumps over the lazy dog": "敏捷的棕色狐狸跳过懒狗",
+ "this layer": "此图层",
+ "This will remove the selected {0}.": "这会移除所选{0}。",
+ "This will remove the selected components and every reference to them.": "这会移除所选组件及其所有引用。",
+ "Toggle custom max scale ratio": "切换自定义最大缩放比例",
+ "Toggle custom min scale ratio": "切换自定义最小缩放比例",
+ "Tool call failed.": "工具调用失败。",
+ "Top offset": "顶部偏移",
+ "Tree background options": "树背景选项",
+ "Type a class name or selector to add or create": "输入要添加或创建的类名或选择器",
+ "Type that scales.": "随尺寸缩放的排版。",
+ "Unbound control": "未绑定控件",
+ "Unhide": "取消隐藏",
+ "Unhide selected": "取消隐藏所选项",
+ "unsupported-model": "不支持的模型",
+ "Update \"{0}\" shade count": "更新“{0}”深色阶数量",
+ "Update \"{0}\" tint count": "更新“{0}”浅色阶数量",
+ "Update token \"{0}\"": "更新令牌“{0}”",
+ "Upload media": "上传媒体",
+ "wrap": "wrap",
+ "Wrap {0} {1} in folder": "将{0}{1}放入文件夹",
+ "Wrap reverse": "反向换行",
+ "Wrap selection in": "将所选内容包裹到",
+ "wrap-reverse": "wrap-reverse",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/spotlight.ts b/src/admin/i18n/locales/zh-CN/spotlight.ts
new file mode 100644
index 000000000..e284c543c
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/spotlight.ts
@@ -0,0 +1,233 @@
+export const spotlightZhCN = {
+ "{0}px": "{0}px",
+ "About Instatic": "关于 Instatic",
+ "Add a field to the currently open table": "向当前打开的数据表添加字段",
+ "Add a new content collection": "添加新的内容集合",
+ "Add a new reusable Visual Component to the site": "向站点添加可复用的可视化组件",
+ "Argument collection": "参数填写",
+ "Ask AI…": "询问 AI…",
+ "Ask anything…": "询问任何问题…",
+ "Browse all keyboard shortcuts": "浏览所有键盘快捷键",
+ "Browse and execute registered plugin commands": "浏览并执行已注册的插件命令",
+ "Browse and insert media assets": "浏览并插入媒体资源",
+ "Browse and manage color design tokens": "浏览和管理颜色设计 Token",
+ "Browse and manage spacing design tokens": "浏览和管理间距设计 Token",
+ "Browse and manage typography design tokens": "浏览和管理排版设计 Token",
+ "Change the active canvas viewport": "更改当前画布视口",
+ "Change the title of the active page": "更改当前页面标题",
+ "Command palette": "命令面板",
+ "Command results": "命令结果",
+ "Component name": "组件名称",
+ "Configure publishing and deployment settings": "配置发布和部署设置",
+ "Copy browser, OS, and version info for bug reports": "复制浏览器、操作系统和版本信息,用于错误报告",
+ "Copy environment info": "复制环境信息",
+ "Copy layer": "复制图层",
+ "Copy the selected layer to the clipboard": "将所选图层复制到剪贴板",
+ "Copy title": "副本标题",
+ "Create a copy of the active page": "创建当前页面的副本",
+ "Create a folder to organize uploaded files": "创建文件夹以整理上传文件",
+ "Create a new admin user and send an invite": "创建新的后台用户并发送邀请",
+ "Create a new blog post, article, or content entry": "创建新的博客文章、文章或内容条目",
+ "Create a new collection (group of documents)": "创建新的内容集合(文档组)",
+ "Create a new data table": "创建新的数据表",
+ "Create a new folder in the media library": "在媒体库中创建新文件夹",
+ "Create a new page in the current site": "在当前站点中创建新页面",
+ "Create a new structured-data table": "创建新的结构化数据表",
+ "Create a new untitled document in the current collection": "在当前内容集合中创建未命名文档",
+ "Create collection…": "创建内容集合…",
+ "Cut layer": "剪切图层",
+ "Cut the selected layer to the clipboard": "将所选图层剪切到剪贴板",
+ "Cycle panel focus": "循环切换面板焦点",
+ "Decrease canvas zoom level": "缩小画布",
+ "Define a new role with custom capabilities": "定义具有自定义权限的新角色",
+ "Delete current page": "删除当前页面",
+ "Delete layer": "删除图层",
+ "Design tokens overview — colors, typography, spacing": "设计 Token 概览——颜色、排版、间距",
+ "Download a full or partial site bundle — pages, media, folders, redirects": "下载完整或部分站点数据包,包括页面、媒体、文件夹和重定向",
+ "Duplicate current page…": "复制当前页面…",
+ "Duplicate layer": "复制图层",
+ "Duplicate the selected layer": "复制所选图层",
+ "e.g. About Us": "例如:关于我们",
+ "e.g. About Us (copy)": "例如:关于我们(副本)",
+ "e.g. Add a hero section with a blue background": "例如:添加一个蓝色背景的主视觉区域",
+ "e.g. Hero Section": "例如:主视觉区域",
+ "e.g. HeroSection": "例如:HeroSection",
+ "Edit profile": "编辑个人资料",
+ "Editor": "编辑器",
+ "Editor preferences and auto-save settings": "编辑器偏好和自动保存设置",
+ "End your current session": "结束当前会话",
+ "Enter": "回车",
+ "Enter {0}": "输入{0}",
+ "Enter value…": "输入值…",
+ "Exit Visual Component mode": "退出可视化组件模式",
+ "Export Site": "导出站点",
+ "General, Shortcuts, Publishing, Preferences": "常规、快捷键、发布、偏好设置",
+ "Give the selected layer a custom label": "为所选图层设置自定义名称",
+ "Go to Account": "前往账户",
+ "Go to Content": "前往内容",
+ "Go to Data": "前往数据",
+ "Go to Media": "前往媒体",
+ "Go to Plugins": "前往插件",
+ "Go to Site editor": "前往站点编辑器",
+ "Go to Users": "前往用户",
+ "Hide or show the selected layer": "隐藏或显示所选图层",
+ "Import pages or CMS bundles from files, folders, or .zip archives": "从文件、文件夹或 .zip 压缩包导入页面或 CMS 数据包",
+ "Import Site": "导入站点",
+ "Import, re-import, or remove the Core Framework preset": "导入、重新导入或移除核心框架预设",
+ "Increase canvas zoom level": "放大画布",
+ "Install plugin…": "安装插件…",
+ "Invite user…": "邀请用户…",
+ "Keyboard shortcuts": "键盘快捷键",
+ "Keyboard Shortcuts": "键盘快捷键",
+ "Layer label": "图层名称",
+ "Lock or unlock the selected layer": "锁定或解锁所选图层",
+ "Manage content documents": "管理内容文档",
+ "Manage Core Framework": "管理核心框架",
+ "Manage installed plugins": "管理已安装插件",
+ "Manage structured data tables": "管理结构化数据表",
+ "Manage uploaded media files": "管理已上传的媒体文件",
+ "Manage users and roles": "管理用户和角色",
+ "Manage your profile and security": "管理个人资料和安全设置",
+ "Move keyboard focus between canvas, layers, and properties": "在画布、图层和属性之间移动键盘焦点",
+ "Move layer down": "下移图层",
+ "Move layer up": "上移图层",
+ "Move selection to the first child of the current layer": "将选择移到当前图层的第一个子图层",
+ "Move selection to the parent of the current layer": "将选择移到当前图层的父图层",
+ "Move the selected layer one position down": "将所选图层下移一位",
+ "Move the selected layer one position up": "将所选图层上移一位",
+ "My folder": "我的文件夹",
+ "Navigate": "导航",
+ "Navigate to a different page": "导航到其他页面",
+ "New content collection…": "新建内容集合…",
+ "New content document…": "新建内容文档…",
+ "New data table…": "新建数据表…",
+ "New field in current table…": "在当前表中新建字段…",
+ "New folder…": "新建文件夹…",
+ "New media folder…": "新建媒体文件夹…",
+ "New page…": "新建页面…",
+ "New role…": "新建角色…",
+ "New table…": "新建数据表…",
+ "New title": "新标题",
+ "New Visual Component…": "新建可视化组件…",
+ "Next": "下一步",
+ "No commands available": "没有可用命令",
+ "No options match \"": "没有选项匹配“",
+ "No results for": "没有结果:",
+ "Open a Visual Component for editing": "打开可视化组件进行编辑",
+ "Open AI Assistant": "打开 AI 助手",
+ "Open Colors": "打开颜色",
+ "Open content document": "打开内容文档",
+ "Open documentation": "打开文档",
+ "Open file": "打开文件",
+ "Open file in code editor": "在代码编辑器中打开文件",
+ "Open Framework panel": "打开框架面板",
+ "Open GitHub Issues to report a bug or request a feature": "打开 GitHub Issues 以报告错误或请求功能",
+ "Open Media picker…": "打开媒体选择器…",
+ "Open plugin": "打开插件",
+ "Open Settings": "打开设置",
+ "Open Settings → Preferences": "打开设置 → 偏好设置",
+ "Open Settings → Publishing": "打开设置 → 发布",
+ "Open Spacing": "打开间距",
+ "Open the AI assistant panel": "打开 AI 助手面板",
+ "Open the Explorer panel on the Code (stylesheets / scripts) tab": "打开资源管理器面板的代码(样式表/脚本)标签页",
+ "Open the Explorer panel on the Layers (DOM tree) tab": "打开资源管理器面板的图层(DOM 树)标签页",
+ "Open the Explorer panel on the Media (asset library) tab": "打开资源管理器面板的媒体(资源库)标签页",
+ "Open the Explorer panel on the Site (pages / templates / components) tab": "打开资源管理器面板的站点(页面/模板/组件)标签页",
+ "Open the visual editor": "打开可视化编辑器",
+ "Open Typography": "打开排版",
+ "Open user": "打开用户",
+ "Open Visual Component": "打开可视化组件",
+ "Open Visual Component…": "打开可视化组件…",
+ "Parse HTML and insert as page nodes": "解析 HTML 并作为页面节点插入",
+ "Paste from the clipboard into the selected layer": "将剪贴板内容粘贴到所选图层",
+ "Paste layer": "粘贴图层",
+ "Permanently remove the active page": "永久移除当前页面",
+ "Plugin pages": "插件页面",
+ "Press ↵ again to confirm": "再次按 ↵ 确认",
+ "Press Enter again to confirm": "再次按回车确认",
+ "Publish the current draft to production": "将当前草稿发布到生产环境",
+ "Redo": "重做",
+ "Redo the last undone change": "重做上次撤销的更改",
+ "Remove the selected layer from the page": "从页面中移除所选图层",
+ "Rename current page…": "重命名当前页面…",
+ "Rename layer…": "重命名图层…",
+ "Report an issue": "报告问题",
+ "Reset the canvas to 1:1 zoom": "将画布重置为 1:1 缩放",
+ "Reset zoom to 100%": "将缩放重置为 100%",
+ "Return to the page canvas": "返回页面画布",
+ "Run": "运行",
+ "Run plugin command": "运行插件命令",
+ "Run plugin command…": "运行插件命令…",
+ "Scope: {0}": "作用域:{0}",
+ "Search commands": "搜索命令",
+ "Search content…": "搜索内容…",
+ "Search editor commands…": "搜索编辑器命令…",
+ "Search media files…": "搜索媒体文件…",
+ "Search pages…": "搜索页面…",
+ "Search plugin commands…": "搜索插件命令…",
+ "Search plugins…": "搜索插件…",
+ "Search settings sections…": "搜索设置分区…",
+ "Search shortcuts…": "搜索快捷键…",
+ "Search site files…": "搜索站点文件…",
+ "Search tables…": "搜索数据表…",
+ "Search users…": "搜索用户…",
+ "Search viewports…": "搜索视口…",
+ "Search Visual Components…": "搜索可视化组件…",
+ "Select first child layer": "选择第一个子图层",
+ "Select parent layer": "选择父图层",
+ "Send a prompt to the AI assistant": "向 AI 助手发送提示词",
+ "Set: {0}…": "设置:{0}…",
+ "Show Code": "显示代码",
+ "Show keyboard shortcuts": "显示键盘快捷键",
+ "Show Layers": "显示图层",
+ "Show Media": "显示媒体",
+ "Show or hide the AI assistant panel": "显示或隐藏 AI 助手面板",
+ "Show or hide the CSS selectors panel": "显示或隐藏 CSS 选择器面板",
+ "Show or hide the design-token framework panel (colors, type, space)": "显示或隐藏设计 Token 框架面板(颜色、排版、间距)",
+ "Show or hide the Explorer (Layers / Pages / Media) panel": "显示或隐藏资源管理器(图层/页面/媒体)面板",
+ "Show or hide the floating code editor": "显示或隐藏浮动代码编辑器",
+ "Show or hide the properties panel": "显示或隐藏属性面板",
+ "Show or hide the site dependencies panel": "显示或隐藏站点依赖面板",
+ "Show Site": "显示站点",
+ "Site files": "站点文件",
+ "Site name, description, and general settings": "站点名称、描述和常规设置",
+ "Switch between edit and preview mode": "在编辑与预览模式之间切换",
+ "Switch to page": "切换到页面",
+ "Switch to page…": "切换到页面…",
+ "Switch to Pan mode": "切换到平移模式",
+ "Switch to Select mode": "切换到选择模式",
+ "Switch viewport": "切换视口",
+ "Switch viewport…": "切换视口…",
+ "Toggle AI Assistant panel": "切换 AI 助手面板",
+ "Toggle Code editor panel": "切换代码编辑器面板",
+ "Toggle Dependencies panel": "切换依赖面板",
+ "Toggle Explorer panel": "切换资源管理器面板",
+ "Toggle Framework panel": "切换框架面板",
+ "Toggle layer lock": "切换图层锁定",
+ "Toggle layer visibility": "切换图层可见性",
+ "Toggle preview": "切换预览",
+ "Toggle Properties panel": "切换属性面板",
+ "Toggle Selectors panel": "切换选择器面板",
+ "Toggle: {0}": "切换:{0}",
+ "Type a command or search…": "输入命令或搜索…",
+ "Undo": "撤销",
+ "Undo the last change": "撤销上次更改",
+ "Update your name, email, and avatar": "更新姓名、邮箱和头像",
+ "Upload a new image, video, or document": "上传新的图片、视频或文档",
+ "Upload a plugin package (.zip)": "上传插件包(.zip)",
+ "Upload and install a plugin package": "上传并安装插件包",
+ "Upload file…": "上传文件…",
+ "Upload images, videos, or other files": "上传图片、视频或其他文件",
+ "Upload media…": "上传媒体…",
+ "Use the hand tool to pan the canvas": "使用抓手工具平移画布",
+ "Use the pointer to select and edit layers": "使用指针选择和编辑图层",
+ "Version information and license": "版本信息和许可证",
+ "View and customize keyboard shortcuts": "查看和自定义键盘快捷键",
+ "View the Instatic docs": "查看 Instatic 文档",
+ "Visual Component": "可视化组件",
+ "Wrap layer in container": "用容器包裹图层",
+ "Wrap the selected layer in a new container": "用新容器包裹所选图层",
+ "Zoom in": "放大",
+ "Zoom out": "缩小",
+ "{0} — Press Enter again to confirm": "{0} — 再按一次 Enter 确认",
+} as const satisfies Record
diff --git a/src/admin/i18n/locales/zh-CN/users.ts b/src/admin/i18n/locales/zh-CN/users.ts
new file mode 100644
index 000000000..9a69be653
--- /dev/null
+++ b/src/admin/i18n/locales/zh-CN/users.ts
@@ -0,0 +1,110 @@
+export const usersZhCN = {
+ "{0} logged in": "{0} 已登录",
+ "{0} logged out": "{0} 已退出",
+ "{0} pack was installed": "已安装 {0} 资源包",
+ "{0} role changed": "{0} 的角色已更改",
+ "{0} settings were updated": "{0} 的设置已更新",
+ "{0} was created": "已创建 {0}",
+ "{0} was deleted": "已删除 {0}",
+ "{0} was disabled": "已禁用 {0}",
+ "{0} was enabled": "已启用 {0}",
+ "{0} was installed": "已安装 {0}",
+ "{0} was locked": "{0} 已锁定",
+ "{0} was suspended": "{0} 已停用",
+ "{0} was unlocked": "{0} 已解锁",
+ "{0} was updated": "已更新 {0}",
+ "Activate": "启用",
+ "AI chat in {0} completed": "{0}中的 AI 对话已完成",
+ "AI chat in {0} failed": "{0}中的 AI 对话失败",
+ "AI chat in {0} started": "{0}中的 AI 对话已开始",
+ "AI credential": "AI 凭据",
+ "AI credential {0} was created": "已创建 AI 凭据 {0}",
+ "AI credential {0} was deleted": "已删除 AI 凭据 {0}",
+ "AI credential {0} was tested": "已测试 AI 凭据 {0}",
+ "AI credential {0} was updated": "已更新 AI 凭据 {0}",
+ "AI default for {0} was cleared": "已清除{0}的 AI 默认值",
+ "AI default for {0} was updated": "已更新{0}的 AI 默认值",
+ "capabilities": "权限",
+ "capability": "权限",
+ "Data row {0} author changed": "数据行 {0} 的作者已更改",
+ "Data row {0} schedule was canceled": "数据行 {0} 的计划已取消",
+ "Data row {0} status changed": "数据行 {0} 的状态已更改",
+ "Data row {0} was created": "已创建数据行 {0}",
+ "Data row {0} was deleted": "已删除数据行 {0}",
+ "Data row {0} was moved": "已移动数据行 {0}",
+ "Data row {0} was published": "已发布数据行 {0}",
+ "Data row {0} was scheduled": "已计划数据行 {0}",
+ "Data row {0} was updated": "已更新数据行 {0}",
+ "Data table {0} was created": "已创建数据表 {0}",
+ "Data table {0} was deleted": "已删除数据表 {0}",
+ "Data table {0} was updated": "已更新数据表 {0}",
+ "Edit Role": "编辑角色",
+ "Edit User": "编辑用户",
+ "Failed login for {0}": "{0} 登录失败",
+ "Login rate limit hit": "已触发登录频率限制",
+ "Login rate limit hit for {0}": "{0} 已触发登录频率限制",
+ "MCP connector was created": "已创建 MCP 连接器",
+ "MCP connector was revoked": "已撤销 MCP 连接器",
+ "Password changed for {0}": "已修改 {0} 的密码",
+ "Reset password": "重置密码",
+ "Reset Password": "重置密码",
+ "Save Role": "保存角色",
+ "Save User": "保存用户",
+ "scope": "作用域",
+ "Site was published": "站点已发布",
+ "Suspend": "停用",
+ "unknown row": "未知数据行",
+ "unknown table": "未知数据表",
+ "Users sections": "用户页面分区",
+ "View": "查看",
+ "View Role": "查看角色",
+ "{0} account{1} with admin access.": "{0} 个拥有后台访问权限的账户。",
+ "Access": "访问权限",
+ "Actions": "操作",
+ "Actor": "操作者",
+ "AI": "AI",
+ "All Users": "所有用户",
+ "Audit": "审计",
+ "Audit events": "审计事件",
+ "Audit Events": "审计事件",
+ "Capabilities": "权限",
+ "Content editor": "内容编辑者",
+ "content-editor": "内容编辑者",
+ "Create Role": "创建角色",
+ "Create User": "创建用户",
+ "Custom role": "自定义角色",
+ "Description": "说明",
+ "Details": "详情",
+ "Display name": "显示名称",
+ "Email": "邮箱",
+ "Event": "事件",
+ "Initial password": "初始密码",
+ "Last login": "上次登录",
+ "Leave blank to keep current password": "留空以保留当前密码",
+ "Loading audit events": "正在加载审计事件",
+ "Loading roles": "正在加载角色",
+ "Loading users": "正在加载用户",
+ "Manage admin access, custom roles, and security audit events.": "管理后台访问权限、自定义角色和安全审计事件。",
+ "New password": "新密码",
+ "No admin capabilities": "没有后台权限",
+ "No audit events yet.": "暂无审计事件。",
+ "No description": "无说明",
+ "No roles configured.": "尚未配置角色。",
+ "No users yet.": "暂无用户。",
+ "Owner account": "所有者账户",
+ "Password must be at least 12 characters": "密码至少需要 12 个字符",
+ "Role": "角色",
+ "Role {0}": "角色:{0}",
+ "Roles": "角色",
+ "Runtime & storage": "运行时与存储",
+ "Security and access changes across the admin area.": "后台中的安全和访问权限变更。",
+ "Suspended": "已停用",
+ "System role": "系统角色",
+ "System roles are fixed. Custom roles can be edited.": "系统角色不可修改;自定义角色可以编辑。",
+ "Time": "时间",
+ "Type": "类型",
+ "User": "用户",
+ "User {0}": "用户:{0}",
+ "Users & Roles": "用户与角色",
+ "What can someone with this role do?": "拥有此角色的用户可以执行哪些操作?",
+} as const satisfies Record
diff --git a/src/admin/i18n/runtime.ts b/src/admin/i18n/runtime.ts
new file mode 100644
index 000000000..9257b6ad2
--- /dev/null
+++ b/src/admin/i18n/runtime.ts
@@ -0,0 +1,22 @@
+import type { AdminLocale } from './catalog'
+
+let activeLocale: AdminLocale = 'zh-CN'
+
+export function setActiveAdminLocale(locale: AdminLocale): void {
+ activeLocale = locale
+}
+
+export function localizeAdminLiteral(english: string, chinese: string): string {
+ return activeLocale === 'zh-CN' ? chinese : english
+}
+
+export function formatAdminLiteral(
+ english: string,
+ chinese: string,
+ values: readonly unknown[],
+): string {
+ return localizeAdminLiteral(english, chinese).replace(/\{(\d+)\}/g, (placeholder, index: string) => {
+ const value = values[Number(index)]
+ return value === undefined ? placeholder : String(value)
+ })
+}
diff --git a/src/admin/main.tsx b/src/admin/main.tsx
index ead466299..89af196a9 100644
--- a/src/admin/main.tsx
+++ b/src/admin/main.tsx
@@ -7,6 +7,7 @@ import { AdminContextMenuGuard } from './shared/AdminContextMenuGuard'
import { AdminZoomGuard } from './shared/AdminZoomGuard'
import { ErrorBoundary, flattenErrorChain, logErrorChain } from '@ui/components/ErrorBoundary'
import { ToastProvider, pushToast } from '@ui/components/Toast'
+import { I18nProvider } from './i18n'
import '../styles/globals.css'
// `installPluginRuntime()` used to be called here, eagerly. That dragged
@@ -110,14 +111,16 @@ await Promise.resolve()
flushSync(() => {
root.render(
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ ,
)
})
diff --git a/src/admin/pages/dashboard/widgets/StatusWidget.tsx b/src/admin/pages/dashboard/widgets/StatusWidget.tsx
index 7ab828ded..c2e021e3f 100644
--- a/src/admin/pages/dashboard/widgets/StatusWidget.tsx
+++ b/src/admin/pages/dashboard/widgets/StatusWidget.tsx
@@ -7,13 +7,13 @@ import type { DashboardWidgetRendererProps } from '@core/dashboard'
import { Widget } from '@ui/components/Widget'
import styles from './widgets.module.css'
-interface Row { label: string; value: string; tone: 'green' | 'amber' }
+interface Row { label: string; valueLabel: string; tone: 'green' | 'amber' }
const ROWS: readonly Row[] = [
- { label: 'Site', value: 'Live', tone: 'green' },
- { label: 'Build', value: '3m ago', tone: 'green' },
- { label: 'Backup', value: '2h ago', tone: 'green' },
- { label: 'Plugins', value: '1 update', tone: 'amber' },
+ { label: 'Site', valueLabel: 'Live', tone: 'green' },
+ { label: 'Build', valueLabel: '3m ago', tone: 'green' },
+ { label: 'Backup', valueLabel: '2h ago', tone: 'green' },
+ { label: 'Plugins', valueLabel: '1 update', tone: 'amber' },
]
export function StatusWidget({ span, editing }: DashboardWidgetRendererProps) {
@@ -33,7 +33,7 @@ export function StatusWidget({ span, editing }: DashboardWidgetRendererProps) {
{r.label}
- {r.value}
+ {r.valueLabel}
))}
diff --git a/src/admin/preauth/AdminPreAuthForm.module.css b/src/admin/preauth/AdminPreAuthForm.module.css
index 37699825d..b1cd4484f 100644
--- a/src/admin/preauth/AdminPreAuthForm.module.css
+++ b/src/admin/preauth/AdminPreAuthForm.module.css
@@ -1,13 +1,25 @@
-.brandRow {
+.headerRow {
display: flex;
align-items: center;
+ justify-content: space-between;
gap: var(--space-m);
margin-bottom: var(--space-4xl);
+}
+
+.brandRow {
+ display: flex;
+ align-items: center;
+ gap: var(--space-m);
+ min-width: 0;
color: var(--text-muted);
font-size: var(--text-s);
font-weight: 600;
}
+.languageSwitcher {
+ flex-shrink: 0;
+}
+
.brandIcon {
display: grid;
width: 28px;
diff --git a/src/admin/preauth/AdminPreAuthForm.tsx b/src/admin/preauth/AdminPreAuthForm.tsx
index 12950bd28..909314e94 100644
--- a/src/admin/preauth/AdminPreAuthForm.tsx
+++ b/src/admin/preauth/AdminPreAuthForm.tsx
@@ -15,6 +15,7 @@ import {
import panelStyles from '../AdminEntry.module.css'
import styles from './AdminPreAuthForm.module.css'
import { getErrorMessage } from '@core/utils/errorMessage'
+import { LanguageSwitcher, useI18n, type MessageKey } from '../i18n'
// Phase the unauthenticated form can be in. 'mfa' is a sub-state reached
// only after a login submit returns `mfaRequired: true` — never set by the
@@ -30,15 +31,27 @@ interface AdminPreAuthFormProps {
}
interface PhaseCopy {
- title: string
- submit: string
- submitPending: string
+ title: MessageKey
+ submit: MessageKey
+ submitPending: MessageKey
}
const PHASE_COPY: Record = {
- setup: { title: 'Set Up CMS', submit: 'Create Admin', submitPending: 'Setting up' },
- login: { title: 'Admin Login', submit: 'Sign In', submitPending: 'Signing in' },
- mfa: { title: 'Two-Factor Authentication', submit: 'Verify', submitPending: 'Verifying' },
+ setup: {
+ title: 'preauth.setup.title',
+ submit: 'preauth.setup.submit',
+ submitPending: 'preauth.setup.submitPending',
+ },
+ login: {
+ title: 'preauth.login.title',
+ submit: 'preauth.login.submit',
+ submitPending: 'preauth.login.submitPending',
+ },
+ mfa: {
+ title: 'preauth.mfa.title',
+ submit: 'preauth.mfa.submit',
+ submitPending: 'preauth.mfa.submitPending',
+ },
}
const MIN_PASSWORD_LENGTH = 12
@@ -67,7 +80,8 @@ export function AdminPreAuthForm({
onPhaseChange,
onAuthenticated,
}: AdminPreAuthFormProps) {
- const [siteName, setSiteName] = useState('My Site')
+ const { t } = useI18n()
+ const [siteName, setSiteName] = useState(() => t('preauth.setup.defaultSiteName'))
const [displayName, setDisplayName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
@@ -84,14 +98,14 @@ export function AdminPreAuthForm({
async function handleSetup(event: FormEvent) {
event.preventDefault()
if (password.length < MIN_PASSWORD_LENGTH) {
- setError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`)
+ setError(t('preauth.error.passwordTooShort', { min: MIN_PASSWORD_LENGTH }))
return
}
await runAuthAction(async () => {
await setupCms({ siteName, email, password, displayName })
await loginCms({ email, password })
onAuthenticated(await getCurrentCmsUser())
- }, 'Setup failed', setSubmitting, setError)
+ }, t('preauth.error.setupFailed'), setSubmitting, setError)
}
async function handleLogin(event: FormEvent) {
@@ -105,7 +119,7 @@ export function AdminPreAuthForm({
return
}
onAuthenticated(await getCurrentCmsUser())
- }, 'Login failed', setSubmitting, setError)
+ }, t('preauth.error.loginFailed'), setSubmitting, setError)
}
async function handleMfaVerify(event: FormEvent) {
@@ -115,11 +129,11 @@ export function AdminPreAuthForm({
const user = await getCurrentCmsUser()
setMfaCode('')
onAuthenticated(user)
- }, 'MFA verification failed', setSubmitting, setError)
+ }, t('preauth.error.mfaVerificationFailed'), setSubmitting, setError)
}
const copy = PHASE_COPY[phase]
- const submitLabel = submitting ? copy.submitPending : copy.submit
+ const submitLabel = t(submitting ? copy.submitPending : copy.submit)
// Pre-auth brand row: when the install has picked a favicon, render it
// in place of the default icon AND swap the "Instatic" label for
@@ -135,29 +149,32 @@ export function AdminPreAuthForm({
return (
-