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 (
-
- {publicSite.faviconUrl ? ( - - ) : ( - - )} - {brandLabel} +
+
+ {publicSite.faviconUrl ? ( + + ) : ( + + )} + {brandLabel} +
+
-

{copy.title}

+

{t(copy.title)}

{phase === 'mfa' ? (