From b9c1bf37d4619f34728e28aab46a17ed73a73c93 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:07:23 +0900 Subject: [PATCH 1/4] ci: check translation key consistency --- .githooks/pre-commit | 13 ++ .githooks/pre-push | 24 +++ .github/workflows/translation-consistency.yml | 20 ++ package.json | 2 + scripts/check-translations.mjs | 175 ++++++++++++++++++ scripts/git-staged-snapshot.mjs | 19 ++ scripts/install-git-hooks.mjs | 10 + 7 files changed, 263 insertions(+) create mode 100755 .githooks/pre-commit create mode 100755 .githooks/pre-push create mode 100644 .github/workflows/translation-consistency.yml create mode 100755 scripts/check-translations.mjs create mode 100755 scripts/git-staged-snapshot.mjs create mode 100755 scripts/install-git-hooks.mjs diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..15eff2aa --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,13 @@ +#!/bin/sh + +set -eu + +if git diff --cached --quiet -- 'public/locales/*/translations.json'; then + exit 0 +fi + +snapshot_dir=$(mktemp -d) +trap 'rm -rf "$snapshot_dir"' EXIT + +node scripts/git-staged-snapshot.mjs "$(pwd)" "$snapshot_dir" +pnpm run check:i18n -- --root="$snapshot_dir" --base-ref=HEAD --staged diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 00000000..4e555e81 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,24 @@ +#!/bin/sh + +set -eu + +locale_changed=0 +base_ref='' +while read -r local_ref local_oid remote_ref remote_oid; do + if [ "$local_oid" = "0000000000000000000000000000000000000000" ]; then + continue + fi + if [ "$remote_oid" = "0000000000000000000000000000000000000000" ]; then + if git diff-tree --no-commit-id --name-only -r "$local_oid" -- 'public/locales/*/translations.json' | grep -q .; then + locale_changed=1 + base_ref=$(git merge-base "$local_oid" origin/main 2>/dev/null || git rev-parse "$local_oid^") + fi + elif ! git diff --quiet "$remote_oid..$local_oid" -- 'public/locales/*/translations.json'; then + locale_changed=1 + base_ref=$remote_oid + fi +done + +if [ "$locale_changed" -eq 1 ]; then + pnpm run check:i18n -- --base-ref="$base_ref" +fi diff --git a/.github/workflows/translation-consistency.yml b/.github/workflows/translation-consistency.yml new file mode 100644 index 00000000..90b68d22 --- /dev/null +++ b/.github/workflows/translation-consistency.yml @@ -0,0 +1,20 @@ +name: Translation consistency + +on: + push: + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10.33.0 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm run check:i18n -- --base-ref=origin/main diff --git a/package.json b/package.json index 720745ee..a2ecc9c7 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "packageManager": "pnpm@10.33.0", "scripts": { + "prepare": "node scripts/install-git-hooks.mjs", "dev": "pnpm run predev && next dev", "build": "pnpm run prebuild && next build", "start": "next start", @@ -12,6 +13,7 @@ "e2e:mock-api": "ts-node --project ./tsconfig.node.json ./tests/e2e/mock-api.ts", "lint": "eslint .", "lint-fix": "eslint . --fix", + "check:i18n": "node ./scripts/check-translations.mjs", "predev": "ts-node --project ./tsconfig.node.json ./scripts/prebuild.tsx", "prebuild": "cross-env NODE_ENV=production ts-node --project ./tsconfig.node.json ./scripts/prebuild.tsx" }, diff --git a/scripts/check-translations.mjs b/scripts/check-translations.mjs new file mode 100755 index 00000000..b1edae45 --- /dev/null +++ b/scripts/check-translations.mjs @@ -0,0 +1,175 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +const REFERENCE_LOCALE = 'zh-CN'; + +function flattenKeys(value, prefix = '', keys = new Set()) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + keys.add(prefix); + return keys; + } + for (const [key, child] of Object.entries(value)) { + flattenKeys(child, prefix ? `${prefix}.${key}` : key, keys); + } + return keys; +} + +function readTranslation(filePath) { + try { + return flattenKeys(JSON.parse(readFileSync(filePath, 'utf8'))); + } catch (error) { + throw new Error(`${filePath} is not valid JSON: ${error.message}`); + } +} + +function changedFiles(baseRef, root, staged) { + if (!baseRef) return []; + try { + const diffArgs = staged ? ['diff', '--cached', '--name-only', baseRef] : ['diff', '--name-only', baseRef]; + return execFileSync('git', [...diffArgs, '--', 'public/locales/*/translations.json'], { + cwd: root, + encoding: 'utf8', + }) + .trim() + .split('\n') + .filter(Boolean); + } catch (error) { + throw new Error(`Unable to inspect translation changes against ${baseRef}: ${error.message}`); + } +} + +function readReferenceKeys(baseRef, relativePath, root) { + try { + return flattenKeys(JSON.parse(execFileSync('git', ['show', `${baseRef}:${relativePath}`], { cwd: root, encoding: 'utf8' }))); + } catch (error) { + if (error.status === 128) return new Set(); + throw new Error(`Unable to read ${relativePath} from ${baseRef}: ${error.message}`); + } +} + +export function runCheck(root = process.cwd(), baseRef, staged = false, repoRoot = process.cwd()) { + const localesRoot = path.join(root, 'public/locales'); + const errors = []; + + if (!existsSync(localesRoot) || !statSync(localesRoot).isDirectory()) { + return ['public/locales directory does not exist.']; + } + + const localeDirs = readdirSync(localesRoot) + .filter((entry) => statSync(path.join(localesRoot, entry)).isDirectory()) + .sort(); + + if (!localeDirs.includes(REFERENCE_LOCALE)) { + errors.push(`Reference locale "${REFERENCE_LOCALE}" is missing from public/locales.`); + return errors; + } + + const referencePath = path.join(localesRoot, REFERENCE_LOCALE, 'translations.json'); + if (!existsSync(referencePath)) { + errors.push(`${referencePath} is missing.`); + return errors; + } + + try { + readTranslation(referencePath); + } catch (error) { + errors.push(error.message); + return errors; + } + + const localeKeysByName = new Map(); + for (const locale of localeDirs) { + const filePath = path.join(localesRoot, locale, 'translations.json'); + if (!existsSync(filePath)) { + errors.push(`${filePath} is missing.`); + continue; + } + + let localeKeys; + try { + localeKeys = readTranslation(filePath); + } catch (error) { + errors.push(error.message); + continue; + } + localeKeysByName.set(locale, localeKeys); + } + + if (baseRef) { + let files; + try { + files = changedFiles(baseRef, repoRoot, staged); + } catch (error) { + errors.push(error.message); + return errors; + } + + for (const relativePath of files) { + const filePath = path.join(root, relativePath); + const oldKeys = readReferenceKeys(baseRef, relativePath, repoRoot); + const newKeys = existsSync(filePath) ? readTranslation(filePath) : new Set(); + const added = [...newKeys].filter((key) => !oldKeys.has(key)).sort(); + const removed = [...oldKeys].filter((key) => !newKeys.has(key)).sort(); + + for (const key of added) { + const missingLocales = localeDirs.filter((name) => !localeKeysByName.get(name)?.has(key)); + if (missingLocales.length > 0) { + errors.push( + `${relativePath} added "${key}", but it is missing from: ${missingLocales + .map((name) => `public/locales/${name}/translations.json`) + .join(', ')}` + ); + } + } + for (const key of removed) { + const remainingLocales = localeDirs.filter((name) => localeKeysByName.get(name)?.has(key)); + if (remainingLocales.length > 0) { + errors.push( + `${relativePath} removed "${key}", but it is still present in: ${remainingLocales + .map((name) => `public/locales/${name}/translations.json`) + .join(', ')}` + ); + } + } + } + } + + return errors; +} + +function main() { + const rootArg = process.argv.find((arg) => arg.startsWith('--root=')); + const baseRefArg = process.argv.find((arg) => arg.startsWith('--base-ref=')); + const staged = process.argv.includes('--staged'); + const repoRoot = process.cwd(); + const root = rootArg ? path.resolve(rootArg.slice('--root='.length)) : process.cwd(); + const baseRef = baseRefArg?.slice('--base-ref='.length) || resolveDefaultBaseRef(repoRoot); + const errors = runCheck(root, baseRef, staged, repoRoot); + + if (errors.length > 0) { + console.error('Translation consistency check failed.'); + for (const error of errors) console.error(`\n${error}`); + process.exitCode = 1; + return; + } + console.log(`Translation consistency check passed for ${REFERENCE_LOCALE} and all locale files.`); +} + +function resolveDefaultBaseRef(root) { + for (const candidate of ['origin/main', 'HEAD^']) { + try { + return execFileSync('git', ['rev-parse', '--verify', candidate], { cwd: root, encoding: 'utf8' }).trim(); + } catch { + // Try the next available base. + } + } + return undefined; +} + +const scriptPath = process.argv[1] && path.resolve(process.argv[1]); +if (scriptPath === path.resolve(fileURLToPath(import.meta.url))) main(); diff --git a/scripts/git-staged-snapshot.mjs b/scripts/git-staged-snapshot.mjs new file mode 100755 index 00000000..1921529e --- /dev/null +++ b/scripts/git-staged-snapshot.mjs @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { mkdirSync } from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +const [repoRoot, destination] = process.argv.slice(2); +if (!repoRoot || !destination) { + console.error('Usage: git-staged-snapshot.mjs '); + process.exit(1); +} + +mkdirSync(destination, { recursive: true }); +const prefix = destination.endsWith(path.sep) ? destination : `${destination}${path.sep}`; +execFileSync('git', ['checkout-index', '-a', '-f', `--prefix=${prefix}`], { + cwd: repoRoot, + stdio: 'inherit', +}); diff --git a/scripts/install-git-hooks.mjs b/scripts/install-git-hooks.mjs new file mode 100755 index 00000000..d89b5f9c --- /dev/null +++ b/scripts/install-git-hooks.mjs @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; + +try { + execFileSync('git', ['rev-parse', '--git-dir'], { stdio: 'ignore' }); + execFileSync('git', ['config', 'core.hooksPath', '.githooks'], { stdio: 'inherit' }); +} catch { + // Package installation is also used outside a Git checkout. +} From c94f31415130eeba3ed08a12af23fb3f16fe7d3d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:12:30 +0900 Subject: [PATCH 2/4] fix: validate changed locales against en-US --- scripts/check-translations.mjs | 47 ++++++++++++---------------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/scripts/check-translations.mjs b/scripts/check-translations.mjs index b1edae45..57463de2 100755 --- a/scripts/check-translations.mjs +++ b/scripts/check-translations.mjs @@ -6,7 +6,7 @@ import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; -const REFERENCE_LOCALE = 'zh-CN'; +const REFERENCE_LOCALE = 'en-US'; function flattenKeys(value, prefix = '', keys = new Set()) { if (!value || typeof value !== 'object' || Array.isArray(value)) { @@ -43,13 +43,11 @@ function changedFiles(baseRef, root, staged) { } } -function readReferenceKeys(baseRef, relativePath, root) { - try { - return flattenKeys(JSON.parse(execFileSync('git', ['show', `${baseRef}:${relativePath}`], { cwd: root, encoding: 'utf8' }))); - } catch (error) { - if (error.status === 128) return new Set(); - throw new Error(`Unable to read ${relativePath} from ${baseRef}: ${error.message}`); - } +function diffKeys(referenceKeys, localeKeys) { + return { + missing: [...referenceKeys].filter((key) => !localeKeys.has(key)).sort(), + extra: [...localeKeys].filter((key) => !referenceKeys.has(key)).sort(), + }; } export function runCheck(root = process.cwd(), baseRef, staged = false, repoRoot = process.cwd()) { @@ -110,31 +108,20 @@ export function runCheck(root = process.cwd(), baseRef, staged = false, repoRoot } for (const relativePath of files) { + const locale = relativePath.split('/')[2]; + if (locale === REFERENCE_LOCALE) continue; + const filePath = path.join(root, relativePath); - const oldKeys = readReferenceKeys(baseRef, relativePath, repoRoot); const newKeys = existsSync(filePath) ? readTranslation(filePath) : new Set(); - const added = [...newKeys].filter((key) => !oldKeys.has(key)).sort(); - const removed = [...oldKeys].filter((key) => !newKeys.has(key)).sort(); - - for (const key of added) { - const missingLocales = localeDirs.filter((name) => !localeKeysByName.get(name)?.has(key)); - if (missingLocales.length > 0) { - errors.push( - `${relativePath} added "${key}", but it is missing from: ${missingLocales - .map((name) => `public/locales/${name}/translations.json`) - .join(', ')}` - ); - } + const referenceKeys = localeKeysByName.get(REFERENCE_LOCALE); + if (!referenceKeys) continue; + const { missing, extra } = diffKeys(referenceKeys, newKeys); + + if (missing.length > 0) { + errors.push(`${relativePath} is missing ${missing.length} key(s) from ${REFERENCE_LOCALE}:\n ${missing.join('\n ')}`); } - for (const key of removed) { - const remainingLocales = localeDirs.filter((name) => localeKeysByName.get(name)?.has(key)); - if (remainingLocales.length > 0) { - errors.push( - `${relativePath} removed "${key}", but it is still present in: ${remainingLocales - .map((name) => `public/locales/${name}/translations.json`) - .join(', ')}` - ); - } + if (extra.length > 0) { + errors.push(`${relativePath} has ${extra.length} key(s) not present in ${REFERENCE_LOCALE}:\n ${extra.join('\n ')}`); } } } From 3d5aee5824b7780409de856bdf26a1d747cebddb Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:15:12 +0900 Subject: [PATCH 3/4] ci: report locale parity status --- .github/workflows/translation-consistency.yml | 2 +- scripts/check-translations.mjs | 34 +++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/translation-consistency.yml b/.github/workflows/translation-consistency.yml index 90b68d22..b814fcaf 100644 --- a/.github/workflows/translation-consistency.yml +++ b/.github/workflows/translation-consistency.yml @@ -17,4 +17,4 @@ jobs: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - - run: pnpm run check:i18n -- --base-ref=origin/main + - run: pnpm run check:i18n -- --all diff --git a/scripts/check-translations.mjs b/scripts/check-translations.mjs index 57463de2..9fb0b89c 100755 --- a/scripts/check-translations.mjs +++ b/scripts/check-translations.mjs @@ -50,7 +50,7 @@ function diffKeys(referenceKeys, localeKeys) { }; } -export function runCheck(root = process.cwd(), baseRef, staged = false, repoRoot = process.cwd()) { +export function runCheck(root = process.cwd(), baseRef, staged = false, repoRoot = process.cwd(), allLocales = false) { const localesRoot = path.join(root, 'public/locales'); const errors = []; @@ -98,7 +98,34 @@ export function runCheck(root = process.cwd(), baseRef, staged = false, repoRoot localeKeysByName.set(locale, localeKeys); } - if (baseRef) { + if (allLocales) { + console.log(`✅ ${REFERENCE_LOCALE} is the reference locale.`); + const referenceKeys = localeKeysByName.get(REFERENCE_LOCALE); + if (referenceKeys) { + for (const locale of localeDirs) { + if (locale === REFERENCE_LOCALE) continue; + const localeKeys = localeKeysByName.get(locale); + if (!localeKeys) { + console.log(`❌ ${locale} could not be checked because translations.json is missing or invalid.`); + continue; + } + const { missing, extra } = diffKeys(referenceKeys, localeKeys); + if (missing.length === 0 && extra.length === 0) { + console.log(`✅ ${locale} matches ${REFERENCE_LOCALE}.`); + continue; + } + console.log(`❌ ${locale} does not match ${REFERENCE_LOCALE}.`); + if (missing.length > 0) { + errors.push(`public/locales/${locale}/translations.json is missing ${missing.length} key(s) from ${REFERENCE_LOCALE}:\n ${missing.join('\n ')}`); + } + if (extra.length > 0) { + errors.push(`public/locales/${locale}/translations.json has ${extra.length} key(s) not present in ${REFERENCE_LOCALE}:\n ${extra.join('\n ')}`); + } + } + } + } + + if (baseRef && !allLocales) { let files; try { files = changedFiles(baseRef, repoRoot, staged); @@ -133,10 +160,11 @@ function main() { const rootArg = process.argv.find((arg) => arg.startsWith('--root=')); const baseRefArg = process.argv.find((arg) => arg.startsWith('--base-ref=')); const staged = process.argv.includes('--staged'); + const allLocales = process.argv.includes('--all'); const repoRoot = process.cwd(); const root = rootArg ? path.resolve(rootArg.slice('--root='.length)) : process.cwd(); const baseRef = baseRefArg?.slice('--base-ref='.length) || resolveDefaultBaseRef(repoRoot); - const errors = runCheck(root, baseRef, staged, repoRoot); + const errors = runCheck(root, baseRef, staged, repoRoot, allLocales); if (errors.length > 0) { console.error('Translation consistency check failed.'); From 4d35594fb15145825604b2ad93ea237b849daeca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Mon, 10 Aug 2026 11:37:37 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(i18n):=20=E8=A1=A5=E9=BD=90=E5=A4=9A?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E7=BC=BA=E5=A4=B1=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/locales/de-DE/translations.json | 228 +++++++++++- public/locales/en-US/translations.json | 59 ++- public/locales/ja-JP/translations.json | 156 +++++++- public/locales/ru-RU/translations.json | 156 +++++++- public/locales/vi-VN/translations.json | 482 ++++++++++++++++++++++++- public/locales/zh-TW/translations.json | 150 +++++++- 6 files changed, 1176 insertions(+), 55 deletions(-) diff --git a/public/locales/de-DE/translations.json b/public/locales/de-DE/translations.json index 9819ea41..442c5803 100644 --- a/public/locales/de-DE/translations.json +++ b/public/locales/de-DE/translations.json @@ -59,7 +59,10 @@ "similarity": "Similarity Detection", "ai_review": "AI Review", "ai_review_records": "Review Records", - "ai_review_fullscan": "Full Scan" + "ai_review_fullscan": "Full Scan", + "announcements": "Announcements", + "script_audits": "Script Audit", + "reports": "Report management" }, "advertise": { "title": "Anzeigenverwaltung", @@ -277,7 +280,13 @@ "title": "OIDC-Anbieterverwaltung", "type_oauth2": "OAuth2 (GitHub)", "type_oidc": "OIDC", - "update_success": "Erfolgreich aktualisiert" + "update_success": "Erfolgreich aktualisiert", + "col_icon": "Icon", + "discover_button": "Auto Discover", + "discover_failed": "OIDC discovery failed. Please check the URL.", + "discover_success": "OIDC configuration discovered successfully", + "icon_upload": "Upload Icon", + "icon_upload_success": "Icon uploaded successfully" }, "scores": { "action_delete": "Löschen", @@ -355,7 +364,12 @@ "unwell_no": "Angemessen", "unwell_yes": "Unangemessen", "visibility_success": "Sichtbarkeit erfolgreich aktualisiert", - "visibility_title": "Skript-Sichtbarkeit ändern" + "visibility_title": "Skript-Sichtbarkeit ändern", + "delete_reason": "Reason (optional)", + "delete_reason_placeholder": "Record the reason for this deletion (saved with the audit log)", + "search_field_content": "Content", + "search_field_description": "Description", + "search_field_name": "Name" }, "system_config": { "ai_api_key": "API-Schlüssel", @@ -380,7 +394,18 @@ "es_index_hint": "Startet im Hintergrund einen vollständigen Neuaufbau des Skript-Suchindex. Die API-Antwort bedeutet nur, dass die Aufgabe gestartet wurde; den Fortschritt bitte in den Backend-Logs prüfen.", "es_index_button": "ES-Index neu aufbauen", "es_index_confirm": "ES-Suchindex jetzt neu aufbauen? Bei großen Datenbanken kann das länger dauern.", - "es_index_started": "Neuaufbau des ES-Suchindex gestartet" + "es_index_started": "Neuaufbau des ES-Suchindex gestartet", + "migrate_avatar_title": "Avatar Migration", + "migrate_avatar_button": "Start Migration", + "migrate_avatar_confirm": "Are you sure you want to start avatar migration? This will download user avatars from UCenter and save them locally.", + "migrate_avatar_started": "Avatar migration started", + "migrate_avatar_progress": "Migrated: {migrated} / Skipped: {skipped} / Failed: {failed} / Total: {total}", + "ranking_title": "Ranking Recompute", + "recompute_trending_hint": "The daily pick (trending_score) is recomputed automatically every day at 03:10. After changing ranking parameters, trigger a full recompute here to see the effect immediately.", + "recompute_trending_button": "Recompute trending_score", + "recompute_trending_confirm": "Recompute trending_score for all scripts now? Large databases may take a few seconds to tens of seconds.", + "recompute_trending_started": "trending_score recompute started", + "recompute_trending_running": "A recompute is already running; not retriggered" }, "users": { "action_admin_level": "Stufe ändern", @@ -406,7 +431,8 @@ "status_banned": "Gesperrt", "title": "Benutzerverwaltung", "unban_confirm": "Möchten Sie diesen Benutzer wirklich entsperren?", - "unban_success": "Benutzer erfolgreich entsperrt" + "unban_success": "Benutzer erfolgreich entsperrt", + "col_register_ip": "Register IP" }, "similarity": { "tab_pairs": "Pairs", @@ -512,6 +538,79 @@ "jj_encode": "JJEncode-Kodierung erkannt", "eval_density": "eval/dynamische Ausführungsdichte zu hoch" } + }, + "announcements": { + "title": "Announcement Management", + "action_create": "Create", + "action_edit": "Edit", + "action_delete": "Delete", + "create_title": "Create Announcement", + "edit_title": "Edit Announcement", + "col_title": "Title", + "col_level": "Level", + "col_status": "Status", + "col_createtime": "Created", + "col_actions": "Actions", + "field_title": "Title", + "field_content": "Content", + "field_level": "Level", + "field_status": "Status", + "level_normal": "Normal", + "level_important": "Important", + "status_enabled": "Enabled", + "status_disabled": "Disabled", + "create_success": "Announcement created successfully", + "update_success": "Announcement updated successfully", + "delete_success": "Announcement deleted successfully", + "delete_confirm": "Are you sure you want to delete this announcement?", + "at_least_one_language": "Please fill in the title for at least one language" + }, + "script_audits": { + "title": "Script Audit", + "col_script": "Script", + "col_version": "Version", + "col_submitter": "Submitter", + "col_status": "Status", + "col_createtime": "Submitted At", + "col_actions": "Actions", + "status_pending": "Pending", + "status_approved": "Approved", + "status_rejected": "Rejected", + "filter_status": "Status", + "filter_all": "All", + "filter_script_name_placeholder": "Search script name", + "refresh": "Refresh", + "action_review": "Review", + "action_approve": "Approve", + "action_reject": "Reject", + "approve_success": "Approved", + "reject_success": "Rejected", + "drawer_title": "Audit Detail", + "detail_script": "Script", + "detail_submitter": "Submitter", + "detail_changelog": "Changelog", + "detail_reason": "Audit Reason / Note", + "detail_reject_reason": "Reject Reason", + "detail_code": "Code", + "approve_modal_title": "Approve Audit", + "approve_reason_label": "Approval note (optional, for AI false-positive correction or audit basis)", + "approve_reason_placeholder": "If this was an AI false positive, note the manual review basis; leave blank to approve directly", + "reject_modal_title": "Reject Audit", + "reject_reason_label": "Reject reason (sent to author via in-app notification)", + "reject_reason_required": "Please provide a reject reason", + "reject_reason_placeholder": "Explain why this script is rejected (e.g. malicious code, policy violation, copyright issue)" + }, + "reports": { + "title": "Report management", + "col_script": "Script", + "col_reporter": "Reporter", + "col_reason": "Reason", + "col_status": "Status", + "col_comments": "Comments", + "col_createtime": "Created at", + "col_actions": "Actions", + "action_view": "View", + "filter_status": "Filter by status" } }, "auth": { @@ -816,7 +915,11 @@ "error_qq_migrate_unavailable": "Der QQ-Login-Migrationsdienst ist derzeit nicht verfügbar, bitte verwenden Sie eine andere Anmeldemethode", "error_invalid_params": "Ungültige Parameter, bitte versuchen Sie es erneut", "error_qq_migrate_failed": "QQ-Login-Migration fehlgeschlagen, bitte versuchen Sie es später erneut", - "error_unknown": "Beim Anmelden ist ein Fehler aufgetreten, bitte versuchen Sie es erneut" + "error_unknown": "Beim Anmelden ist ein Fehler aufgetreten, bitte versuchen Sie es erneut", + "qq_migrate_deprecation_title": "QQ sign-in will soon be discontinued", + "qq_migrate_deprecation_content": "QQ sign-in will soon be discontinued. After signing in, please bind another sign-in method on the settings page to avoid losing access in the future.", + "qq_migrate_deprecation_continue": "Continue signing in", + "qq_migrate_deprecation_cancel": "Cancel" }, "notifications": { "access": { @@ -1162,6 +1265,78 @@ "metadata": "Berechtigungen", "ratings": "Bewertungen", "versions": "Versionen" + }, + "metadata": { + "empty_description": "This script does not declare displayable permissions, connections, or run rules.", + "empty_title": "No metadata details", + "fields": { + "author": "Author", + "connect": "Network access", + "exclude": "Exclude rules", + "grant": "Permissions", + "include": "Include rules", + "license": "License", + "match": "Match rules", + "namespace": "Namespace", + "require": "External dependencies", + "resource": "External resources", + "run_at": "Run timing" + }, + "grants": { + "addElement": "Allows the script to create and insert elements into the page.", + "addStyle": "Allows the script to inject CSS into the page.", + "deleteValue": "Allows the script to delete data stored by the userscript manager.", + "download": "Allows the script to trigger file downloads.", + "getResourceText": "Allows the script to read text resources declared with @resource.", + "getResourceUrl": "Allows the script to read URLs for resources declared with @resource.", + "getValue": "Allows the script to read data stored by the userscript manager.", + "info": "Allows the script to read script and manager runtime information.", + "listValues": "Allows the script to list stored data keys.", + "none": "The script declares that it needs no extra userscript-manager permissions.", + "notification": "Allows the script to show system or browser notifications.", + "openInTab": "Allows the script to open new browser tabs.", + "registerMenuCommand": "Allows the script to add commands to the userscript manager menu.", + "setClipboard": "Allows the script to write to the clipboard.", + "setValue": "Allows the script to save data in the userscript manager.", + "unknown": "An extended permission requested by the script; behavior depends on manager support.", + "unregisterMenuCommand": "Allows the script to remove registered menu commands.", + "unsafeWindow": "Allows access to the page window object and direct interaction with page scripts.", + "xmlhttpRequest": "Allows network requests proxied by the userscript manager." + }, + "intro": "These details come from the userscript metadata block and help explain declared permissions, run scope, and external dependencies.", + "run_at": { + "context_menu": "Runs only when triggered from the context menu.", + "document_body": "Runs after the page body is available.", + "document_end": "Runs around DOM load completion.", + "document_idle": "Runs when the page is mostly loaded and idle.", + "document_start": "Runs as early as possible while the page starts loading.", + "unknown": "A declared run timing whose behavior depends on the userscript manager." + }, + "sections": { + "author": "Author information declared by the script.", + "connect": "Domains the script may connect to, usually used with network request permissions.", + "exclude": "URL rules excluded from the run scope.", + "grant": "Capabilities or API permissions requested from the userscript manager.", + "include": "URL rules where the script declares it can run.", + "license": "Open source or usage license declared by the script.", + "match": "URL rules matched by the script.", + "namespace": "Namespace used to distinguish the script identity or source.", + "require": "External JavaScript dependencies loaded before the script runs.", + "resource": "External resources declared for script access.", + "run_at": "The page lifecycle stage where the script prefers to run." + }, + "values": { + "author": "Declared author value.", + "connect": "Allowed domain or wildcard connection rule.", + "exclude": "URL rule excluded from execution.", + "include": "URL rule included for execution.", + "license": "License identifier.", + "match": "URL rule matched for execution.", + "namespace": "Namespace identifier.", + "require": "External script dependency URL.", + "resource": "External resource declaration.", + "unknown": "Metadata declaration value." + } } }, "diff": { @@ -2178,7 +2353,33 @@ "processed_col_script": "Skript", "processed_col_operator": "Bearbeiter", "processed_col_time": "Zeit", - "processed_col_reason": "Grund" + "processed_col_reason": "Grund", + "account_deactivated": "This account has been deactivated", + "ban_user": "Ban user", + "unban_user": "Unban user", + "ban_reason": "Ban reason", + "ban_reason_placeholder": "Enter a reason for the ban", + "ban_duration": "Ban duration", + "ban_permanent": "Permanent ban", + "ban_1_day": "1 day", + "ban_3_days": "3 days", + "ban_7_days": "7 days", + "ban_30_days": "30 days", + "ban_90_days": "90 days", + "ban_custom": "Custom", + "ban_clean_options": "Cleanup options", + "ban_clean_scores": "Remove user ratings", + "ban_clean_scripts": "Remove user scripts", + "ban_success": "User banned", + "unban_success": "User unbanned", + "ban_confirm_title": "Confirm ban", + "unban_confirm": "Are you sure you want to unban this user?", + "user_banned": "Banned", + "ban_expire_at": "Ban expires at", + "ban_permanent_label": "Permanent", + "register_ip": "Registration IP", + "ip_location": "IP location", + "register_email": "Registration email" }, "save": "Speichern", "script_list": { @@ -2297,7 +2498,15 @@ "cancel_failed": "Abbruch fehlgeschlagen", "applied_at": "Beantragt am", "effective_at": "Wirksam am" - } + }, + "contact_email": "Contact email", + "email_code_placeholder": "Enter the 6-digit verification code", + "email_code_required": "Send and enter the email verification code first", + "email_code_resend": "Resend", + "email_code_sent": "Verification code sent", + "email_help_text": "Displayed publicly so others can contact you. This can differ from your registration email and may be left blank.", + "email_send_code": "Send verification code", + "email_send_code_countdown": "Resend in {{seconds}}s" }, "ads": { "label": "Anzeige", @@ -2321,5 +2530,8 @@ "title": "Code Failed Integrity Check", "help": "If this is a false positive, please apply for an exemption via the admin contact listed in the site FAQ." } + }, + "announcements": { + "title": "Announcements" } } diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index a9207398..e42ec464 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -87,7 +87,8 @@ "script_audits": "Script Audit", "ai_review": "AI Review", "ai_review_records": "Review Records", - "ai_review_fullscan": "Full Scan" + "ai_review_fullscan": "Full Scan", + "reports": "Report management" }, "advertise": { "title": "Ad Management", @@ -598,6 +599,18 @@ "jj_encode": "JJEncode encoding detected", "eval_density": "eval/dynamic execution call density too high" } + }, + "reports": { + "title": "Report management", + "col_script": "Script", + "col_reporter": "Reporter", + "col_reason": "Reason", + "col_status": "Status", + "col_comments": "Comments", + "col_createtime": "Created at", + "col_actions": "Actions", + "action_view": "View", + "filter_status": "Filter by status" } }, "auth": { @@ -910,7 +923,11 @@ "error_qq_migrate_unavailable": "QQ login migration service is currently unavailable, please use another login method", "error_invalid_params": "Invalid parameters, please try again", "error_qq_migrate_failed": "QQ login migration failed, please try again later", - "error_unknown": "An error occurred during login, please try again" + "error_unknown": "An error occurred during login, please try again", + "qq_migrate_deprecation_title": "QQ sign-in will soon be discontinued", + "qq_migrate_deprecation_content": "QQ sign-in will soon be discontinued. After signing in, please bind another sign-in method on the settings page to avoid losing access in the future.", + "qq_migrate_deprecation_continue": "Continue signing in", + "qq_migrate_deprecation_cancel": "Cancel" }, "notifications": { "access": { @@ -2344,7 +2361,33 @@ "processed_col_script": "Script", "processed_col_operator": "Operator", "processed_col_time": "Time", - "processed_col_reason": "Reason" + "processed_col_reason": "Reason", + "account_deactivated": "This account has been deactivated", + "ban_user": "Ban user", + "unban_user": "Unban user", + "ban_reason": "Ban reason", + "ban_reason_placeholder": "Enter a reason for the ban", + "ban_duration": "Ban duration", + "ban_permanent": "Permanent ban", + "ban_1_day": "1 day", + "ban_3_days": "3 days", + "ban_7_days": "7 days", + "ban_30_days": "30 days", + "ban_90_days": "90 days", + "ban_custom": "Custom", + "ban_clean_options": "Cleanup options", + "ban_clean_scores": "Remove user ratings", + "ban_clean_scripts": "Remove user scripts", + "ban_success": "User banned", + "unban_success": "User unbanned", + "ban_confirm_title": "Confirm ban", + "unban_confirm": "Are you sure you want to unban this user?", + "user_banned": "Banned", + "ban_expire_at": "Ban expires at", + "ban_permanent_label": "Permanent", + "register_ip": "Registration IP", + "ip_location": "IP location", + "register_email": "Registration email" }, "save": "Save", "script_list": { @@ -2463,7 +2506,15 @@ "cancel_failed": "Failed to cancel deletion", "applied_at": "Applied at", "effective_at": "Effective at" - } + }, + "contact_email": "Contact email", + "email_code_placeholder": "Enter the 6-digit verification code", + "email_code_required": "Send and enter the email verification code first", + "email_code_resend": "Resend", + "email_code_sent": "Verification code sent", + "email_help_text": "Displayed publicly so others can contact you. This can differ from your registration email and may be left blank.", + "email_send_code": "Send verification code", + "email_send_code_countdown": "Resend in {{seconds}}s" }, "utils": { "time_format": "YYYY-MM-DD" diff --git a/public/locales/ja-JP/translations.json b/public/locales/ja-JP/translations.json index a3380100..ee6c28e9 100644 --- a/public/locales/ja-JP/translations.json +++ b/public/locales/ja-JP/translations.json @@ -59,7 +59,10 @@ "similarity": "Similarity Detection", "ai_review": "AI Review", "ai_review_records": "Review Records", - "ai_review_fullscan": "Full Scan" + "ai_review_fullscan": "Full Scan", + "announcements": "Announcements", + "script_audits": "Script Audit", + "reports": "Report management" }, "advertise": { "title": "広告管理", @@ -277,7 +280,13 @@ "title": "OIDCプロバイダー管理", "type_oauth2": "OAuth2 (GitHub)", "type_oidc": "OIDC", - "update_success": "更新しました" + "update_success": "更新しました", + "col_icon": "Icon", + "discover_button": "Auto Discover", + "discover_failed": "OIDC discovery failed. Please check the URL.", + "discover_success": "OIDC configuration discovered successfully", + "icon_upload": "Upload Icon", + "icon_upload_success": "Icon uploaded successfully" }, "scores": { "action_delete": "削除", @@ -355,7 +364,12 @@ "unwell_no": "適切", "unwell_yes": "不適切", "visibility_success": "公開設定を更新しました", - "visibility_title": "スクリプトの公開設定を変更" + "visibility_title": "スクリプトの公開設定を変更", + "delete_reason": "Reason (optional)", + "delete_reason_placeholder": "Record the reason for this deletion (saved with the audit log)", + "search_field_content": "Content", + "search_field_description": "Description", + "search_field_name": "Name" }, "system_config": { "ai_api_key": "APIキー", @@ -380,7 +394,18 @@ "es_index_hint": "バックグラウンドでスクリプト検索インデックスを全件再構築します。APIの応答はタスク開始のみを示します。完了状況はバックエンドログで確認してください。", "es_index_button": "ESインデックスを再構築", "es_index_confirm": "ES検索インデックスの再構築を開始しますか?大きなデータベースでは時間がかかる場合があります。", - "es_index_started": "ES検索インデックスの再構築を開始しました" + "es_index_started": "ES検索インデックスの再構築を開始しました", + "migrate_avatar_title": "Avatar Migration", + "migrate_avatar_button": "Start Migration", + "migrate_avatar_confirm": "Are you sure you want to start avatar migration? This will download user avatars from UCenter and save them locally.", + "migrate_avatar_started": "Avatar migration started", + "migrate_avatar_progress": "Migrated: {migrated} / Skipped: {skipped} / Failed: {failed} / Total: {total}", + "ranking_title": "Ranking Recompute", + "recompute_trending_hint": "The daily pick (trending_score) is recomputed automatically every day at 03:10. After changing ranking parameters, trigger a full recompute here to see the effect immediately.", + "recompute_trending_button": "Recompute trending_score", + "recompute_trending_confirm": "Recompute trending_score for all scripts now? Large databases may take a few seconds to tens of seconds.", + "recompute_trending_started": "trending_score recompute started", + "recompute_trending_running": "A recompute is already running; not retriggered" }, "users": { "action_admin_level": "レベルを変更", @@ -406,7 +431,8 @@ "status_banned": "禁止済み", "title": "ユーザー管理", "unban_confirm": "このユーザーの禁止を解除してもよろしいですか?", - "unban_success": "ユーザーの禁止を解除しました" + "unban_success": "ユーザーの禁止を解除しました", + "col_register_ip": "Register IP" }, "similarity": { "tab_pairs": "Pairs", @@ -512,6 +538,79 @@ "jj_encode": "JJEncodeエンコーディングを検出", "eval_density": "eval/動的実行呼び出し密度が高すぎます" } + }, + "announcements": { + "title": "Announcement Management", + "action_create": "Create", + "action_edit": "Edit", + "action_delete": "Delete", + "create_title": "Create Announcement", + "edit_title": "Edit Announcement", + "col_title": "Title", + "col_level": "Level", + "col_status": "Status", + "col_createtime": "Created", + "col_actions": "Actions", + "field_title": "Title", + "field_content": "Content", + "field_level": "Level", + "field_status": "Status", + "level_normal": "Normal", + "level_important": "Important", + "status_enabled": "Enabled", + "status_disabled": "Disabled", + "create_success": "Announcement created successfully", + "update_success": "Announcement updated successfully", + "delete_success": "Announcement deleted successfully", + "delete_confirm": "Are you sure you want to delete this announcement?", + "at_least_one_language": "Please fill in the title for at least one language" + }, + "script_audits": { + "title": "Script Audit", + "col_script": "Script", + "col_version": "Version", + "col_submitter": "Submitter", + "col_status": "Status", + "col_createtime": "Submitted At", + "col_actions": "Actions", + "status_pending": "Pending", + "status_approved": "Approved", + "status_rejected": "Rejected", + "filter_status": "Status", + "filter_all": "All", + "filter_script_name_placeholder": "Search script name", + "refresh": "Refresh", + "action_review": "Review", + "action_approve": "Approve", + "action_reject": "Reject", + "approve_success": "Approved", + "reject_success": "Rejected", + "drawer_title": "Audit Detail", + "detail_script": "Script", + "detail_submitter": "Submitter", + "detail_changelog": "Changelog", + "detail_reason": "Audit Reason / Note", + "detail_reject_reason": "Reject Reason", + "detail_code": "Code", + "approve_modal_title": "Approve Audit", + "approve_reason_label": "Approval note (optional, for AI false-positive correction or audit basis)", + "approve_reason_placeholder": "If this was an AI false positive, note the manual review basis; leave blank to approve directly", + "reject_modal_title": "Reject Audit", + "reject_reason_label": "Reject reason (sent to author via in-app notification)", + "reject_reason_required": "Please provide a reject reason", + "reject_reason_placeholder": "Explain why this script is rejected (e.g. malicious code, policy violation, copyright issue)" + }, + "reports": { + "title": "Report management", + "col_script": "Script", + "col_reporter": "Reporter", + "col_reason": "Reason", + "col_status": "Status", + "col_comments": "Comments", + "col_createtime": "Created at", + "col_actions": "Actions", + "action_view": "View", + "filter_status": "Filter by status" } }, "auth": { @@ -816,7 +915,11 @@ "error_qq_migrate_unavailable": "QQログイン移行サービスは現在利用できません。別のログイン方法をご利用ください", "error_invalid_params": "パラメータが無効です。もう一度お試しください", "error_qq_migrate_failed": "QQログイン移行に失敗しました。しばらくしてからもう一度お試しください", - "error_unknown": "ログイン中にエラーが発生しました。もう一度お試しください" + "error_unknown": "ログイン中にエラーが発生しました。もう一度お試しください", + "qq_migrate_deprecation_title": "QQ sign-in will soon be discontinued", + "qq_migrate_deprecation_content": "QQ sign-in will soon be discontinued. After signing in, please bind another sign-in method on the settings page to avoid losing access in the future.", + "qq_migrate_deprecation_continue": "Continue signing in", + "qq_migrate_deprecation_cancel": "Cancel" }, "notifications": { "access": { @@ -2250,7 +2353,33 @@ "processed_col_script": "スクリプト", "processed_col_operator": "処理者", "processed_col_time": "時間", - "processed_col_reason": "理由" + "processed_col_reason": "理由", + "account_deactivated": "This account has been deactivated", + "ban_user": "Ban user", + "unban_user": "Unban user", + "ban_reason": "Ban reason", + "ban_reason_placeholder": "Enter a reason for the ban", + "ban_duration": "Ban duration", + "ban_permanent": "Permanent ban", + "ban_1_day": "1 day", + "ban_3_days": "3 days", + "ban_7_days": "7 days", + "ban_30_days": "30 days", + "ban_90_days": "90 days", + "ban_custom": "Custom", + "ban_clean_options": "Cleanup options", + "ban_clean_scores": "Remove user ratings", + "ban_clean_scripts": "Remove user scripts", + "ban_success": "User banned", + "unban_success": "User unbanned", + "ban_confirm_title": "Confirm ban", + "unban_confirm": "Are you sure you want to unban this user?", + "user_banned": "Banned", + "ban_expire_at": "Ban expires at", + "ban_permanent_label": "Permanent", + "register_ip": "Registration IP", + "ip_location": "IP location", + "register_email": "Registration email" }, "save": "保存", "script_list": { @@ -2369,7 +2498,15 @@ "cancel_failed": "キャンセルに失敗しました", "applied_at": "申請日時", "effective_at": "有効日時" - } + }, + "contact_email": "Contact email", + "email_code_placeholder": "Enter the 6-digit verification code", + "email_code_required": "Send and enter the email verification code first", + "email_code_resend": "Resend", + "email_code_sent": "Verification code sent", + "email_help_text": "Displayed publicly so others can contact you. This can differ from your registration email and may be left blank.", + "email_send_code": "Send verification code", + "email_send_code_countdown": "Resend in {{seconds}}s" }, "ads": { "label": "広告", @@ -2393,5 +2530,8 @@ "title": "Code Failed Integrity Check", "help": "If this is a false positive, please apply for an exemption via the admin contact listed in the site FAQ." } + }, + "announcements": { + "title": "Announcements" } } diff --git a/public/locales/ru-RU/translations.json b/public/locales/ru-RU/translations.json index a271c11d..aaaec6df 100644 --- a/public/locales/ru-RU/translations.json +++ b/public/locales/ru-RU/translations.json @@ -59,7 +59,10 @@ "similarity": "Similarity Detection", "ai_review": "AI Review", "ai_review_records": "Review Records", - "ai_review_fullscan": "Full Scan" + "ai_review_fullscan": "Full Scan", + "announcements": "Announcements", + "script_audits": "Script Audit", + "reports": "Report management" }, "advertise": { "title": "Управление рекламой", @@ -277,7 +280,13 @@ "title": "Управление OIDC-провайдерами", "type_oauth2": "OAuth2 (GitHub)", "type_oidc": "OIDC", - "update_success": "Успешно обновлено" + "update_success": "Успешно обновлено", + "col_icon": "Icon", + "discover_button": "Auto Discover", + "discover_failed": "OIDC discovery failed. Please check the URL.", + "discover_success": "OIDC configuration discovered successfully", + "icon_upload": "Upload Icon", + "icon_upload_success": "Icon uploaded successfully" }, "scores": { "action_delete": "Удалить", @@ -355,7 +364,12 @@ "unwell_no": "Приемлемый", "unwell_yes": "Неприемлемый", "visibility_success": "Видимость успешно обновлена", - "visibility_title": "Изменить видимость скрипта" + "visibility_title": "Изменить видимость скрипта", + "delete_reason": "Reason (optional)", + "delete_reason_placeholder": "Record the reason for this deletion (saved with the audit log)", + "search_field_content": "Content", + "search_field_description": "Description", + "search_field_name": "Name" }, "system_config": { "ai_api_key": "API-ключ", @@ -380,7 +394,18 @@ "es_index_hint": "Запускает полную перестройку поискового индекса скриптов в фоне. Ответ API означает только запуск задачи; прогресс смотрите в логах backend.", "es_index_button": "Перестроить индекс ES", "es_index_confirm": "Начать перестройку поискового индекса ES? Для больших баз это может занять время.", - "es_index_started": "Перестройка поискового индекса ES запущена" + "es_index_started": "Перестройка поискового индекса ES запущена", + "migrate_avatar_title": "Avatar Migration", + "migrate_avatar_button": "Start Migration", + "migrate_avatar_confirm": "Are you sure you want to start avatar migration? This will download user avatars from UCenter and save them locally.", + "migrate_avatar_started": "Avatar migration started", + "migrate_avatar_progress": "Migrated: {migrated} / Skipped: {skipped} / Failed: {failed} / Total: {total}", + "ranking_title": "Ranking Recompute", + "recompute_trending_hint": "The daily pick (trending_score) is recomputed automatically every day at 03:10. After changing ranking parameters, trigger a full recompute here to see the effect immediately.", + "recompute_trending_button": "Recompute trending_score", + "recompute_trending_confirm": "Recompute trending_score for all scripts now? Large databases may take a few seconds to tens of seconds.", + "recompute_trending_started": "trending_score recompute started", + "recompute_trending_running": "A recompute is already running; not retriggered" }, "users": { "action_admin_level": "Изменить уровень", @@ -406,7 +431,8 @@ "status_banned": "Заблокирован", "title": "Управление пользователями", "unban_confirm": "Вы уверены, что хотите разблокировать этого пользователя?", - "unban_success": "Пользователь успешно разблокирован" + "unban_success": "Пользователь успешно разблокирован", + "col_register_ip": "Register IP" }, "similarity": { "tab_pairs": "Pairs", @@ -512,6 +538,79 @@ "jj_encode": "Обнаружена кодировка JJEncode", "eval_density": "Плотность вызовов eval/динамического выполнения слишком высока" } + }, + "announcements": { + "title": "Announcement Management", + "action_create": "Create", + "action_edit": "Edit", + "action_delete": "Delete", + "create_title": "Create Announcement", + "edit_title": "Edit Announcement", + "col_title": "Title", + "col_level": "Level", + "col_status": "Status", + "col_createtime": "Created", + "col_actions": "Actions", + "field_title": "Title", + "field_content": "Content", + "field_level": "Level", + "field_status": "Status", + "level_normal": "Normal", + "level_important": "Important", + "status_enabled": "Enabled", + "status_disabled": "Disabled", + "create_success": "Announcement created successfully", + "update_success": "Announcement updated successfully", + "delete_success": "Announcement deleted successfully", + "delete_confirm": "Are you sure you want to delete this announcement?", + "at_least_one_language": "Please fill in the title for at least one language" + }, + "script_audits": { + "title": "Script Audit", + "col_script": "Script", + "col_version": "Version", + "col_submitter": "Submitter", + "col_status": "Status", + "col_createtime": "Submitted At", + "col_actions": "Actions", + "status_pending": "Pending", + "status_approved": "Approved", + "status_rejected": "Rejected", + "filter_status": "Status", + "filter_all": "All", + "filter_script_name_placeholder": "Search script name", + "refresh": "Refresh", + "action_review": "Review", + "action_approve": "Approve", + "action_reject": "Reject", + "approve_success": "Approved", + "reject_success": "Rejected", + "drawer_title": "Audit Detail", + "detail_script": "Script", + "detail_submitter": "Submitter", + "detail_changelog": "Changelog", + "detail_reason": "Audit Reason / Note", + "detail_reject_reason": "Reject Reason", + "detail_code": "Code", + "approve_modal_title": "Approve Audit", + "approve_reason_label": "Approval note (optional, for AI false-positive correction or audit basis)", + "approve_reason_placeholder": "If this was an AI false positive, note the manual review basis; leave blank to approve directly", + "reject_modal_title": "Reject Audit", + "reject_reason_label": "Reject reason (sent to author via in-app notification)", + "reject_reason_required": "Please provide a reject reason", + "reject_reason_placeholder": "Explain why this script is rejected (e.g. malicious code, policy violation, copyright issue)" + }, + "reports": { + "title": "Report management", + "col_script": "Script", + "col_reporter": "Reporter", + "col_reason": "Reason", + "col_status": "Status", + "col_comments": "Comments", + "col_createtime": "Created at", + "col_actions": "Actions", + "action_view": "View", + "filter_status": "Filter by status" } }, "auth": { @@ -816,7 +915,11 @@ "error_qq_migrate_unavailable": "Служба миграции QQ-входа временно недоступна, используйте другой способ входа", "error_invalid_params": "Неверные параметры, попробуйте ещё раз", "error_qq_migrate_failed": "Миграция QQ-входа не удалась, попробуйте позже", - "error_unknown": "Произошла ошибка при входе, попробуйте ещё раз" + "error_unknown": "Произошла ошибка при входе, попробуйте ещё раз", + "qq_migrate_deprecation_title": "QQ sign-in will soon be discontinued", + "qq_migrate_deprecation_content": "QQ sign-in will soon be discontinued. After signing in, please bind another sign-in method on the settings page to avoid losing access in the future.", + "qq_migrate_deprecation_continue": "Continue signing in", + "qq_migrate_deprecation_cancel": "Cancel" }, "notifications": { "access": { @@ -2250,7 +2353,33 @@ "processed_col_script": "Скрипт", "processed_col_operator": "Оператор", "processed_col_time": "Время", - "processed_col_reason": "Причина" + "processed_col_reason": "Причина", + "account_deactivated": "This account has been deactivated", + "ban_user": "Ban user", + "unban_user": "Unban user", + "ban_reason": "Ban reason", + "ban_reason_placeholder": "Enter a reason for the ban", + "ban_duration": "Ban duration", + "ban_permanent": "Permanent ban", + "ban_1_day": "1 day", + "ban_3_days": "3 days", + "ban_7_days": "7 days", + "ban_30_days": "30 days", + "ban_90_days": "90 days", + "ban_custom": "Custom", + "ban_clean_options": "Cleanup options", + "ban_clean_scores": "Remove user ratings", + "ban_clean_scripts": "Remove user scripts", + "ban_success": "User banned", + "unban_success": "User unbanned", + "ban_confirm_title": "Confirm ban", + "unban_confirm": "Are you sure you want to unban this user?", + "user_banned": "Banned", + "ban_expire_at": "Ban expires at", + "ban_permanent_label": "Permanent", + "register_ip": "Registration IP", + "ip_location": "IP location", + "register_email": "Registration email" }, "save": "Сохранить", "script_list": { @@ -2369,7 +2498,15 @@ "cancel_failed": "Ошибка отмены", "applied_at": "Дата заявки", "effective_at": "Дата вступления в силу" - } + }, + "contact_email": "Contact email", + "email_code_placeholder": "Enter the 6-digit verification code", + "email_code_required": "Send and enter the email verification code first", + "email_code_resend": "Resend", + "email_code_sent": "Verification code sent", + "email_help_text": "Displayed publicly so others can contact you. This can differ from your registration email and may be left blank.", + "email_send_code": "Send verification code", + "email_send_code_countdown": "Resend in {{seconds}}s" }, "ads": { "label": "Реклама", @@ -2393,5 +2530,8 @@ "title": "Code Failed Integrity Check", "help": "If this is a false positive, please apply for an exemption via the admin contact listed in the site FAQ." } + }, + "announcements": { + "title": "Announcements" } } diff --git a/public/locales/vi-VN/translations.json b/public/locales/vi-VN/translations.json index 0965ce8a..c88d356a 100644 --- a/public/locales/vi-VN/translations.json +++ b/public/locales/vi-VN/translations.json @@ -5,7 +5,9 @@ "script_audit_pending": "Chờ duyệt", "script_create": "Tạo script", "script_delete": "Xóa script", - "script_update": "Cập nhật script" + "script_update": "Cập nhật script", + "script_audit_approved": "Audit Approved", + "script_audit_rejected": "Audit Rejected" }, "description": "Nhật ký thao tác quản trị", "filter": { @@ -30,7 +32,20 @@ "col_reason": "Lý do", "delete_confirm": "Bạn có chắc muốn xóa phản hồi này?", "delete_success": "Đã xóa phản hồi thành công", - "title": "Quản lý phản hồi" + "title": "Quản lý phản hồi", + "copy": "Copy", + "copy_failed": "Copy failed", + "copy_success": "Copied to clipboard", + "hide_empty": "Hide empty content", + "reason_filter_all": "All reasons", + "reasons": { + "better": "Found a better alternative", + "bug": "Encountered an error or bug", + "feature": "Missing a feature I need", + "other": "Other", + "unused": "No longer need ScriptCat" + }, + "search_content_placeholder": "Search content" }, "navigation": { "advertise": "Quản lý quảng cáo", @@ -43,7 +58,11 @@ "users": "Quản lý người dùng", "ai_review": "AI Review", "ai_review_records": "Review Records", - "ai_review_fullscan": "Full Scan" + "ai_review_fullscan": "Full Scan", + "announcements": "Announcements", + "similarity": "Similarity Detection", + "script_audits": "Script Audit", + "reports": "Report management" }, "advertise": { "title": "Quản lý quảng cáo", @@ -261,7 +280,13 @@ "title": "Quản lý nhà cung cấp OIDC", "type_oauth2": "OAuth2 (GitHub)", "type_oidc": "OIDC", - "update_success": "Cập nhật thành công" + "update_success": "Cập nhật thành công", + "col_icon": "Icon", + "discover_button": "Auto Discover", + "discover_failed": "OIDC discovery failed. Please check the URL.", + "discover_success": "OIDC configuration discovered successfully", + "icon_upload": "Upload Icon", + "icon_upload_success": "Icon uploaded successfully" }, "scores": { "action_delete": "Xóa", @@ -320,7 +345,14 @@ "unwell_no": "Phù hợp", "unwell_yes": "Không phù hợp", "visibility_success": "Đã cập nhật hiển thị thành công", - "visibility_title": "Thay đổi hiển thị script" + "visibility_title": "Thay đổi hiển thị script", + "action_set_multiplier": "Set Score Multiplier", + "col_score_multiplier": "Score Multiplier", + "delete_reason": "Reason (optional)", + "delete_reason_placeholder": "Record the reason for this deletion (saved with the audit log)", + "search_field_content": "Content", + "search_field_description": "Description", + "search_field_name": "Name" }, "system_config": { "ai_api_key": "Khóa API", @@ -345,7 +377,18 @@ "es_index_hint": "Thao tác này khởi chạy xây dựng lại toàn bộ chỉ mục tìm kiếm script trong nền. Phản hồi API chỉ cho biết tác vụ đã bắt đầu; hãy xem log backend để theo dõi tiến độ.", "es_index_button": "Xây dựng lại chỉ mục ES", "es_index_confirm": "Bắt đầu xây dựng lại chỉ mục tìm kiếm ES? Cơ sở dữ liệu lớn có thể mất nhiều thời gian.", - "es_index_started": "Đã bắt đầu xây dựng lại chỉ mục tìm kiếm ES" + "es_index_started": "Đã bắt đầu xây dựng lại chỉ mục tìm kiếm ES", + "migrate_avatar_title": "Avatar Migration", + "migrate_avatar_button": "Start Migration", + "migrate_avatar_confirm": "Are you sure you want to start avatar migration? This will download user avatars from UCenter and save them locally.", + "migrate_avatar_started": "Avatar migration started", + "migrate_avatar_progress": "Migrated: {migrated} / Skipped: {skipped} / Failed: {failed} / Total: {total}", + "ranking_title": "Ranking Recompute", + "recompute_trending_hint": "The daily pick (trending_score) is recomputed automatically every day at 03:10. After changing ranking parameters, trigger a full recompute here to see the effect immediately.", + "recompute_trending_button": "Recompute trending_score", + "recompute_trending_confirm": "Recompute trending_score for all scripts now? Large databases may take a few seconds to tens of seconds.", + "recompute_trending_started": "trending_score recompute started", + "recompute_trending_running": "A recompute is already running; not retriggered" }, "users": { "action_admin_level": "Thay đổi cấp", @@ -371,7 +414,203 @@ "status_banned": "Đã cấm", "title": "Quản lý người dùng", "unban_confirm": "Bạn có chắc muốn bỏ cấm người dùng này?", - "unban_success": "Đã bỏ cấm người dùng thành công" + "unban_success": "Đã bỏ cấm người dùng thành công", + "col_register_ip": "Register IP" + }, + "announcements": { + "title": "Announcement Management", + "action_create": "Create", + "action_edit": "Edit", + "action_delete": "Delete", + "create_title": "Create Announcement", + "edit_title": "Edit Announcement", + "col_title": "Title", + "col_level": "Level", + "col_status": "Status", + "col_createtime": "Created", + "col_actions": "Actions", + "field_title": "Title", + "field_content": "Content", + "field_level": "Level", + "field_status": "Status", + "level_normal": "Normal", + "level_important": "Important", + "status_enabled": "Enabled", + "status_disabled": "Disabled", + "create_success": "Announcement created successfully", + "update_success": "Announcement updated successfully", + "delete_success": "Announcement deleted successfully", + "delete_confirm": "Are you sure you want to delete this announcement?", + "at_least_one_language": "Please fill in the title for at least one language" + }, + "script_audits": { + "title": "Script Audit", + "col_script": "Script", + "col_version": "Version", + "col_submitter": "Submitter", + "col_status": "Status", + "col_createtime": "Submitted At", + "col_actions": "Actions", + "status_pending": "Pending", + "status_approved": "Approved", + "status_rejected": "Rejected", + "filter_status": "Status", + "filter_all": "All", + "filter_script_name_placeholder": "Search script name", + "refresh": "Refresh", + "action_review": "Review", + "action_approve": "Approve", + "action_reject": "Reject", + "approve_success": "Approved", + "reject_success": "Rejected", + "drawer_title": "Audit Detail", + "detail_script": "Script", + "detail_submitter": "Submitter", + "detail_changelog": "Changelog", + "detail_reason": "Audit Reason / Note", + "detail_reject_reason": "Reject Reason", + "detail_code": "Code", + "approve_modal_title": "Approve Audit", + "approve_reason_label": "Approval note (optional, for AI false-positive correction or audit basis)", + "approve_reason_placeholder": "If this was an AI false positive, note the manual review basis; leave blank to approve directly", + "reject_modal_title": "Reject Audit", + "reject_reason_label": "Reject reason (sent to author via in-app notification)", + "reject_reason_required": "Please provide a reject reason", + "reject_reason_placeholder": "Explain why this script is rejected (e.g. malicious code, policy violation, copyright issue)" + }, + "scoreMultiplier": { + "title": "Set Score Multiplier — {name}", + "multiplier_label": "Multiplier", + "multiplier_hint": "0 = fully suppressed; 1 = no effect; > 1 = boosted. Range [0, 10]", + "multiplier_required": "Multiplier is required", + "multiplier_range": "Multiplier must be between 0 and 10", + "expire_at_label": "Expires at", + "expire_at_required": "Expiry time is required", + "expire_at_future": "Expiry must be in the future", + "save": "Save", + "cancel": "Cancel", + "clear": "Clear (reset to 1.0)", + "saved": "Saved", + "save_failed": "Save failed", + "cleared": "Cleared", + "clear_failed": "Clear failed" + }, + "similarity": { + "tab_pairs": "Pairs", + "tab_suspects": "Suspects", + "tab_integrity_reviews": "Integrity Reviews", + "tab_pair_whitelist": "Pair Whitelist", + "tab_integrity_whitelist": "Integrity Exemptions", + "col_id": "ID", + "col_script_a": "Script A", + "col_script_b": "Script B", + "col_jaccard": "Jaccard", + "col_common": "Common Fingerprints", + "col_earlier": "Earlier Side", + "col_status": "Status", + "col_integrity": "Integrity Score", + "col_actions": "Actions", + "col_script": "Script", + "col_max_jaccard": "Max Jaccard", + "col_coverage": "External Coverage", + "col_pair_count": "Pair Count", + "col_detected_at": "Detected At", + "col_score": "Score", + "col_createtime": "Created", + "col_reason": "Reason", + "col_added_by": "Added By", + "status_pending": "Pending", + "status_whitelisted": "Whitelisted", + "status_resolved": "Resolved", + "review_pending": "Pending", + "review_ok": "OK", + "review_violated": "Violation", + "action_detail": "Details", + "action_resolve": "Resolve", + "action_whitelist": "Whitelist", + "action_remove": "Remove", + "confirm_remove_whitelist": "Remove this whitelist entry?", + "msg_removed": "Removed", + "modal_add_int_whitelist": "Add Integrity Exemption", + "label_script_id": "Script ID", + "label_reason": "Reason", + "btn_add": "Add", + "modal_resolve_title": "Mark Integrity Review", + "label_decision": "Decision", + "label_note": "Note", + "decision_ok": "OK", + "decision_violated": "Violation", + "msg_review_resolved": "Marked successfully", + "msg_whitelisted": "Added to whitelist", + "drawer_review_detail": "Integrity Review Details", + "label_score": "Total Score", + "label_sub_scores": "Category Scores", + "label_hit_signals": "Hit Signals", + "label_jaccard": "Jaccard", + "label_common": "Common Fingerprints", + "label_earlier": "Earlier Side", + "label_detected_at": "Detected At", + "label_script_a": "Script A", + "label_script_b": "Script B", + "label_code_diff": "Code Diff", + "script_deleted": "Deleted", + "filter_exclude_deleted": "Hide pairs with deleted scripts", + "tab_backfill": "Backfill & Rescan", + "backfill": { + "help_title": "Historical Script Backfill", + "help_body": "After the system is deployed, only newly published or updated scripts are automatically scanned for similarity. To include historical scripts in comparisons, trigger a manual backfill. The backfill sends a scan message for each script, processed asynchronously by background consumers. You can safely leave this page during the process.", + "status_title": "Backfill Status", + "label_running": "Status", + "label_total": "Total", + "label_cursor": "Cursor", + "label_progress": "Progress", + "label_started_at": "Started At", + "label_finished_at": "Finished At", + "state_running": "Running", + "state_idle": "Idle", + "btn_start": "Start Backfill", + "btn_restart": "Restart from Beginning", + "btn_refresh": "Refresh Status", + "confirm_start_title": "Start backfill?", + "confirm_start_body": "This will continue sending scan messages from the last cursor position.", + "confirm_restart_title": "Restart backfill from beginning?", + "confirm_restart_body": "The cursor will reset to 0, and all scripts in the database will be re-scanned. This is usually only needed on initial deployment or after refreshing the stop-fp list.", + "msg_started": "Backfill task started", + "manual_scan_title": "Manually Rescan Single Script", + "manual_scan_placeholder": "Enter script ID", + "btn_manual_scan": "Send Scan", + "msg_manual_scan_published": "Scan message published", + "stop_fp_title": "Stop-fingerprint Refresh", + "stop_fp_warn_title": "Manual trigger usually not needed", + "stop_fp_warn_body": "The stop-fp list is refreshed automatically every hour by a scheduled task. Only trigger manually once after completing the initial full backfill (step 8 in §8.5), so Jaccard calculations filter out common template code.", + "btn_stop_fp_refresh": "Refresh Now", + "msg_stop_fp_refreshed": "Stop-fingerprint set refreshed" + }, + "signal_desc": { + "avg_line_length": "Average line length too high (code may be compressed into few long lines)", + "max_line_length": "Maximum line length too high (contains extremely long lines)", + "whitespace_ratio": "Whitespace ratio too low (code lacks normal spacing and indentation)", + "comment_ratio": "Comment ratio too low (code has almost no comments)", + "single_char_ident_ratio": "Single-character identifier ratio too high (variable names shortened to single characters)", + "hex_ident_ratio": "Hex identifier ratio too high (uses _0x prefixed obfuscated variable names)", + "large_string_array": "Large string array detected (common in obfuscation tool string tables)", + "dean_edwards_packer": "Dean Edwards packer detected", + "aa_encode": "AAEncode encoding detected", + "jj_encode": "JJEncode encoding detected", + "eval_density": "eval/dynamic execution call density too high" + } + }, + "reports": { + "title": "Report management", + "col_script": "Script", + "col_reporter": "Reporter", + "col_reason": "Reason", + "col_status": "Status", + "col_comments": "Comments", + "col_createtime": "Created at", + "col_actions": "Actions", + "action_view": "View", + "filter_status": "Filter by status" } }, "auth": { @@ -676,7 +915,11 @@ "error_qq_migrate_unavailable": "Dịch vụ di chuyển đăng nhập QQ hiện không khả dụng, vui lòng sử dụng phương thức đăng nhập khác", "error_invalid_params": "Tham số không hợp lệ, vui lòng thử lại", "error_qq_migrate_failed": "Di chuyển đăng nhập QQ thất bại, vui lòng thử lại sau", - "error_unknown": "Đã xảy ra lỗi trong quá trình đăng nhập, vui lòng thử lại" + "error_unknown": "Đã xảy ra lỗi trong quá trình đăng nhập, vui lòng thử lại", + "qq_migrate_deprecation_title": "QQ sign-in will soon be discontinued", + "qq_migrate_deprecation_content": "QQ sign-in will soon be discontinued. After signing in, please bind another sign-in method on the settings page to avoid losing access in the future.", + "qq_migrate_deprecation_continue": "Continue signing in", + "qq_migrate_deprecation_cancel": "Cancel" }, "notifications": { "access": { @@ -810,7 +1053,10 @@ "dc_section4_title": "4. Limitation of Liability", "dc_section4_content": "To the maximum extent permitted by law, the Platform and its operators shall not be liable for any direct, indirect, incidental, special, or consequential damages arising from the use or inability to use the Platform services, including but not limited to data loss, loss of profits, or business interruption. The Platform makes no warranties and assumes no responsibility for any goods or services promoted by advertisers through the Platform; any transaction between you and an advertiser is at your own discretion and risk.", "dc_section5_title": "5. Disclaimer Updates", - "dc_section5_content": "We may update this Disclaimer in response to changes in applicable laws or product features. The updated disclaimer will be published on the Platform. We recommend that you review it periodically. Your continued use of the Platform constitutes acceptance of the updated disclaimer. The Platform reserves the right of final interpretation of this disclaimer." + "dc_section5_content": "We may update this Disclaimer in response to changes in applicable laws or product features. The updated disclaimer will be published on the Platform. We recommend that you review it periodically. Your continued use of the Platform constitutes acceptance of the updated disclaimer. The Platform reserves the right of final interpretation of this disclaimer.", + "terms_of_service_description": "Read the terms of service and usage rules for the ScriptCat userscript platform.", + "privacy_policy_description": "Learn how ScriptCat collects, uses, and protects your personal information.", + "disclaimer_description": "Read ScriptCat's disclaimer regarding userscript content and usage risks." }, "script": { "alerts": { @@ -830,7 +1076,20 @@ "deleted_restore_success": "Restored", "deleted_title": "Script has been deleted", "obfuscated_description": "This script has been obfuscated by the author. Although the script site has conducted a review, please be careful when granting dangerous permissions.", - "obfuscated_title": "Script code has been obfuscated" + "obfuscated_title": "Script code has been obfuscated", + "audit_approve_button": "Approve", + "audit_approve_failed": "Approve failed", + "audit_approve_modal_title": "Approve Audit", + "audit_approve_reason_label": "Approval note (optional)", + "audit_approve_reason_placeholder": "If this was an AI false positive, note the manual review basis; leave blank to approve directly", + "audit_approve_success": "Approved", + "audit_reject_button": "Reject", + "audit_reject_failed": "Reject failed", + "audit_reject_modal_title": "Reject Audit", + "audit_reject_reason_label": "Reject reason (sent to author via in-app notification)", + "audit_reject_reason_placeholder": "Explain why this script is rejected, such as malicious code, policy violation, or copyright issue", + "audit_reject_reason_required": "Please provide a reject reason", + "audit_reject_success": "Rejected" }, "card": { "downloads": "Downloads", @@ -934,13 +1193,18 @@ "scheduled_script_tooltip": "This script supports scheduled execution and will run automatically at set intervals: {cron}", "script_details": "Script Details", "tag_label": "Tag: {name}", - "updated_at": "Updated at {time}" + "updated_at": "Updated at {time}", + "license_no_derivative": "No Derivatives", + "license_none": "Not declared", + "license_none_warning": "No license declared — reuse with caution" }, "install": { "install_check_failed": "Failed to check script installation status", "install_script": "Install Script", "reinstall_script": "Reinstall Script v{version}", - "update_script": "Update Script to v{version}" + "update_script": "Update Script to v{version}", + "updated_at": "Updated {time}", + "changelog": "Changelog" }, "installGuide": { "title": "Cần trình quản lý userscript", @@ -973,7 +1237,106 @@ "current_version": "Current Version", "today_installs": "24 giờ qua", "total_installs": "Total Installs", - "user_rating": "User Rating" + "user_rating": "User Rating", + "title": "Statistics" + }, + "metadata": { + "empty_description": "This script does not declare displayable permissions, connections, or run rules.", + "empty_title": "No metadata details", + "fields": { + "author": "Author", + "connect": "Network access", + "exclude": "Exclude rules", + "grant": "Permissions", + "include": "Include rules", + "license": "License", + "match": "Match rules", + "namespace": "Namespace", + "require": "External dependencies", + "resource": "External resources", + "run_at": "Run timing" + }, + "grants": { + "addElement": "Allows the script to create and insert elements into the page.", + "addStyle": "Allows the script to inject CSS into the page.", + "deleteValue": "Allows the script to delete data stored by the userscript manager.", + "download": "Allows the script to trigger file downloads.", + "getResourceText": "Allows the script to read text resources declared with @resource.", + "getResourceUrl": "Allows the script to read URLs for resources declared with @resource.", + "getValue": "Allows the script to read data stored by the userscript manager.", + "info": "Allows the script to read script and manager runtime information.", + "listValues": "Allows the script to list stored data keys.", + "none": "The script declares that it needs no extra userscript-manager permissions.", + "notification": "Allows the script to show system or browser notifications.", + "openInTab": "Allows the script to open new browser tabs.", + "registerMenuCommand": "Allows the script to add commands to the userscript manager menu.", + "setClipboard": "Allows the script to write to the clipboard.", + "setValue": "Allows the script to save data in the userscript manager.", + "unknown": "An extended permission requested by the script; behavior depends on manager support.", + "unregisterMenuCommand": "Allows the script to remove registered menu commands.", + "unsafeWindow": "Allows access to the page window object and direct interaction with page scripts.", + "xmlhttpRequest": "Allows network requests proxied by the userscript manager." + }, + "intro": "These details come from the userscript metadata block and help explain declared permissions, run scope, and external dependencies.", + "run_at": { + "context_menu": "Runs only when triggered from the context menu.", + "document_body": "Runs after the page body is available.", + "document_end": "Runs around DOM load completion.", + "document_idle": "Runs when the page is mostly loaded and idle.", + "document_start": "Runs as early as possible while the page starts loading.", + "unknown": "A declared run timing whose behavior depends on the userscript manager." + }, + "sections": { + "author": "Author information declared by the script.", + "connect": "Domains the script may connect to, usually used with network request permissions.", + "exclude": "URL rules excluded from the run scope.", + "grant": "Capabilities or API permissions requested from the userscript manager.", + "include": "URL rules where the script declares it can run.", + "license": "Open source or usage license declared by the script.", + "match": "URL rules matched by the script.", + "namespace": "Namespace used to distinguish the script identity or source.", + "require": "External JavaScript dependencies loaded before the script runs.", + "resource": "External resources declared for script access.", + "run_at": "The page lifecycle stage where the script prefers to run." + }, + "values": { + "author": "Declared author value.", + "connect": "Allowed domain or wildcard connection rule.", + "exclude": "URL rule excluded from execution.", + "include": "URL rule included for execution.", + "license": "License identifier.", + "match": "URL rule matched for execution.", + "namespace": "Namespace identifier.", + "require": "External script dependency URL.", + "resource": "External resource declaration.", + "unknown": "Metadata declaration value." + } + }, + "permissions": { + "title": "Permissions & access", + "connect": "Cross-origin access", + "grants": "Capabilities", + "more": "+{count}", + "high_risk": "High-risk permission — install with caution", + "cap": { + "net": "Network requests", + "notify": "Notifications", + "clipboard": "Clipboard", + "storage": "Storage", + "tab": "Open tab", + "download": "Download", + "cookie": "Cookies", + "menu": "Menu command" + } + }, + "summary": { + "label": "AI Summary" + }, + "tabs": { + "description": "Description", + "metadata": "Permissions", + "ratings": "Ratings", + "versions": "Versions" } }, "diff": { @@ -1020,7 +1383,18 @@ "version_label": "Version Number", "version_placeholder": "e.g.: 1.0.0", "version_required": "Please enter version number", - "version_tooltip": "Recommend semantic versioning, e.g.: 1.0.0, 2.1.5" + "version_tooltip": "Recommend semantic versioning, e.g.: 1.0.0, 2.1.5", + "license_custom": "Custom", + "license_custom_placeholder": "Enter a license name, e.g. CC-BY-NC-4.0", + "license_group_opensource": "Common open-source licenses", + "license_group_other": "Other", + "license_label": "License", + "license_need_header": "Write the script header (==UserScript==) first to choose a license", + "license_no_derivative": "No Derivatives", + "license_none_warning": "No license declared — reuse with caution", + "license_placeholder": "Select a license", + "license_tooltip": "Writes @license into the script header and ships with the script, giving others a clear reference", + "license_written": "Written to script header // @license {license}" }, "library_section": { "description_label": "Library Description", @@ -1476,7 +1850,13 @@ }, "search": { "page_number": "Page {page}", - "title": "Script Search" + "title": "Script Search", + "domain_subject": "{domain} Userscripts", + "description": "Search Tampermonkey/ScriptCat userscripts on ScriptCat. Discover scripts by website, category, and popularity.", + "keyword_description": "Search userscripts for {keyword} and discover Tampermonkey/ScriptCat scripts to install on ScriptCat.", + "domain_description": "Browse userscripts for {domain} (Tampermonkey/ScriptCat scripts). Discover and install more {domain} scripts on ScriptCat.", + "domain_typed_subject": "{domain} {type}", + "type_description": "Browse all {type} listings on ScriptCat. Discover and install Tampermonkey/ScriptCat scripts." } }, "navigation": { @@ -1578,7 +1958,9 @@ "review_placeholder": "Share your experience to help other users understand this script...", "submit_button": "Submit Review", "update_button": "Update Review", - "your_review_title": "Your Review" + "your_review_title": "Your Review", + "collapsed_hint": "Tap a star to start", + "collapsed_prompt": "Rate this script" } }, "report": { @@ -1772,7 +2154,12 @@ "update_version_failed": "Update version failed:", "version_type_extra": "Prerelease versions are usually for testing new features and may not be stable", "version_type_label": "Version Type", - "view_code_button": "View Code" + "view_code_button": "View Code", + "collapse_changelog": "Collapse", + "expand_changelog": "Show all", + "prerelease_chip": "Pre-release {count}", + "release_chip": "Stable {count}", + "version_count": "Total {count} versions" }, "code_search": { "title": "Tìm mã nguồn", @@ -1957,7 +2344,42 @@ "profile_update_success": "Profile updated successfully!", "report": "Report", "unfollow_success": "Unfollowed", - "verified": "Verified" + "verified": "Verified", + "previously_processed_scripts": "Scripts processed for this user", + "processed_empty": "No processed records", + "processed_action_delete": "Admin Delete", + "processed_action_rejected": "Audit Rejected", + "processed_col_action": "Action", + "processed_col_script": "Script", + "processed_col_operator": "Operator", + "processed_col_time": "Time", + "processed_col_reason": "Reason", + "account_deactivated": "This account has been deactivated", + "ban_user": "Ban user", + "unban_user": "Unban user", + "ban_reason": "Ban reason", + "ban_reason_placeholder": "Enter a reason for the ban", + "ban_duration": "Ban duration", + "ban_permanent": "Permanent ban", + "ban_1_day": "1 day", + "ban_3_days": "3 days", + "ban_7_days": "7 days", + "ban_30_days": "30 days", + "ban_90_days": "90 days", + "ban_custom": "Custom", + "ban_clean_options": "Cleanup options", + "ban_clean_scores": "Remove user ratings", + "ban_clean_scripts": "Remove user scripts", + "ban_success": "User banned", + "unban_success": "User unbanned", + "ban_confirm_title": "Confirm ban", + "unban_confirm": "Are you sure you want to unban this user?", + "user_banned": "Banned", + "ban_expire_at": "Ban expires at", + "ban_permanent_label": "Permanent", + "register_ip": "Registration IP", + "ip_location": "IP location", + "register_email": "Registration email" }, "save": "Save", "script_list": { @@ -2075,7 +2497,16 @@ "cancel_failed": "Hủy thất bại", "applied_at": "Thời gian yêu cầu", "effective_at": "Thời gian hiệu lực" - } + }, + "user_homepage_description": "Browse userscripts published by {username} on ScriptCat.", + "contact_email": "Contact email", + "email_code_placeholder": "Enter the 6-digit verification code", + "email_code_required": "Send and enter the email verification code first", + "email_code_resend": "Resend", + "email_code_sent": "Verification code sent", + "email_help_text": "Displayed publicly so others can contact you. This can differ from your registration email and may be left blank.", + "email_send_code": "Send verification code", + "email_send_code_countdown": "Resend in {{seconds}}s" }, "ads": { "label": "Quảng cáo", @@ -2088,6 +2519,19 @@ "errors": { "api": { "access_not_found": "Quyền không tồn tại" + }, + "integrity_rejected": { + "title": "Code Failed Integrity Check", + "help": "If this is a false positive, please apply for an exemption via the admin contact listed in the site FAQ." + } + }, + "announcements": { + "title": "Announcements" + }, + "similarity": { + "evidence": { + "disclaimer_title": "Preliminary Finding, Not a Final Verdict", + "disclaimer_body": "This page shows automatically detected similar-code evidence. It is informational only. Please do not draw definitive conclusions about the author from this data." } } } diff --git a/public/locales/zh-TW/translations.json b/public/locales/zh-TW/translations.json index bd1e885c..9eb3263f 100644 --- a/public/locales/zh-TW/translations.json +++ b/public/locales/zh-TW/translations.json @@ -59,7 +59,10 @@ "similarity": "相似度檢測", "ai_review": "AI 審核", "ai_review_records": "審核記錄", - "ai_review_fullscan": "全站掃描" + "ai_review_fullscan": "全站掃描", + "announcements": "Announcements", + "script_audits": "Script Audit", + "reports": "Report management" }, "advertise": { "title": "廣告管理", @@ -277,7 +280,13 @@ "title": "OIDC 提供商管理", "type_oauth2": "OAuth2 (GitHub)", "type_oidc": "OIDC", - "update_success": "更新成功" + "update_success": "更新成功", + "col_icon": "Icon", + "discover_button": "Auto Discover", + "discover_failed": "OIDC discovery failed. Please check the URL.", + "discover_success": "OIDC configuration discovered successfully", + "icon_upload": "Upload Icon", + "icon_upload_success": "Icon uploaded successfully" }, "scores": { "action_delete": "刪除", @@ -355,7 +364,12 @@ "unwell_no": "正常", "unwell_yes": "不當", "visibility_success": "可见性更新成功", - "visibility_title": "修改腳本可见性" + "visibility_title": "修改腳本可见性", + "delete_reason": "Reason (optional)", + "delete_reason_placeholder": "Record the reason for this deletion (saved with the audit log)", + "search_field_content": "Content", + "search_field_description": "Description", + "search_field_name": "Name" }, "system_config": { "ai_api_key": "API Key", @@ -386,7 +400,12 @@ "es_index_hint": "觸發後會在後台全量重建腳本搜尋索引。接口返回只表示任務已啟動,完成進度請查看後端日誌。", "es_index_button": "重建 ES 索引", "es_index_confirm": "確定要開始重建 ES 搜尋索引嗎?大庫可能需要較長時間。", - "es_index_started": "ES 搜尋索引重建任務已啟動" + "es_index_started": "ES 搜尋索引重建任務已啟動", + "migrate_avatar_title": "Avatar Migration", + "migrate_avatar_button": "Start Migration", + "migrate_avatar_confirm": "Are you sure you want to start avatar migration? This will download user avatars from UCenter and save them locally.", + "migrate_avatar_started": "Avatar migration started", + "migrate_avatar_progress": "Migrated: {migrated} / Skipped: {skipped} / Failed: {failed} / Total: {total}" }, "users": { "action_admin_level": "修改等級", @@ -412,7 +431,8 @@ "status_banned": "已封禁", "title": "用戶管理", "unban_confirm": "確定要解封此用戶吗?", - "unban_success": "用戶解封成功" + "unban_success": "用戶解封成功", + "col_register_ip": "Register IP" }, "similarity": { "tab_pairs": "相似對", @@ -518,6 +538,79 @@ "jj_encode": "偵測到 JJEncode 編碼", "eval_density": "eval/動態執行呼叫密度過高" } + }, + "announcements": { + "title": "Announcement Management", + "action_create": "Create", + "action_edit": "Edit", + "action_delete": "Delete", + "create_title": "Create Announcement", + "edit_title": "Edit Announcement", + "col_title": "Title", + "col_level": "Level", + "col_status": "Status", + "col_createtime": "Created", + "col_actions": "Actions", + "field_title": "Title", + "field_content": "Content", + "field_level": "Level", + "field_status": "Status", + "level_normal": "Normal", + "level_important": "Important", + "status_enabled": "Enabled", + "status_disabled": "Disabled", + "create_success": "Announcement created successfully", + "update_success": "Announcement updated successfully", + "delete_success": "Announcement deleted successfully", + "delete_confirm": "Are you sure you want to delete this announcement?", + "at_least_one_language": "Please fill in the title for at least one language" + }, + "script_audits": { + "title": "Script Audit", + "col_script": "Script", + "col_version": "Version", + "col_submitter": "Submitter", + "col_status": "Status", + "col_createtime": "Submitted At", + "col_actions": "Actions", + "status_pending": "Pending", + "status_approved": "Approved", + "status_rejected": "Rejected", + "filter_status": "Status", + "filter_all": "All", + "filter_script_name_placeholder": "Search script name", + "refresh": "Refresh", + "action_review": "Review", + "action_approve": "Approve", + "action_reject": "Reject", + "approve_success": "Approved", + "reject_success": "Rejected", + "drawer_title": "Audit Detail", + "detail_script": "Script", + "detail_submitter": "Submitter", + "detail_changelog": "Changelog", + "detail_reason": "Audit Reason / Note", + "detail_reject_reason": "Reject Reason", + "detail_code": "Code", + "approve_modal_title": "Approve Audit", + "approve_reason_label": "Approval note (optional, for AI false-positive correction or audit basis)", + "approve_reason_placeholder": "If this was an AI false positive, note the manual review basis; leave blank to approve directly", + "reject_modal_title": "Reject Audit", + "reject_reason_label": "Reject reason (sent to author via in-app notification)", + "reject_reason_required": "Please provide a reject reason", + "reject_reason_placeholder": "Explain why this script is rejected (e.g. malicious code, policy violation, copyright issue)" + }, + "reports": { + "title": "Report management", + "col_script": "Script", + "col_reporter": "Reporter", + "col_reason": "Reason", + "col_status": "Status", + "col_comments": "Comments", + "col_createtime": "Created at", + "col_actions": "Actions", + "action_view": "View", + "filter_status": "Filter by status" } }, "auth": { @@ -822,7 +915,11 @@ "error_qq_migrate_unavailable": "QQ 登入遷移服務暫時不可用,請使用其他方式登入", "error_invalid_params": "參數錯誤,請重試", "error_qq_migrate_failed": "QQ 登入遷移失敗,請稍後重試", - "error_unknown": "登入過程中出現錯誤,請重試" + "error_unknown": "登入過程中出現錯誤,請重試", + "qq_migrate_deprecation_title": "QQ sign-in will soon be discontinued", + "qq_migrate_deprecation_content": "QQ sign-in will soon be discontinued. After signing in, please bind another sign-in method on the settings page to avoid losing access in the future.", + "qq_migrate_deprecation_continue": "Continue signing in", + "qq_migrate_deprecation_cancel": "Cancel" }, "notifications": { "access": { @@ -2256,7 +2353,33 @@ "processed_col_script": "腳本", "processed_col_operator": "處理人", "processed_col_time": "時間", - "processed_col_reason": "原因" + "processed_col_reason": "原因", + "account_deactivated": "This account has been deactivated", + "ban_user": "Ban user", + "unban_user": "Unban user", + "ban_reason": "Ban reason", + "ban_reason_placeholder": "Enter a reason for the ban", + "ban_duration": "Ban duration", + "ban_permanent": "Permanent ban", + "ban_1_day": "1 day", + "ban_3_days": "3 days", + "ban_7_days": "7 days", + "ban_30_days": "30 days", + "ban_90_days": "90 days", + "ban_custom": "Custom", + "ban_clean_options": "Cleanup options", + "ban_clean_scores": "Remove user ratings", + "ban_clean_scripts": "Remove user scripts", + "ban_success": "User banned", + "unban_success": "User unbanned", + "ban_confirm_title": "Confirm ban", + "unban_confirm": "Are you sure you want to unban this user?", + "user_banned": "Banned", + "ban_expire_at": "Ban expires at", + "ban_permanent_label": "Permanent", + "register_ip": "Registration IP", + "ip_location": "IP location", + "register_email": "Registration email" }, "save": "儲存", "script_list": { @@ -2375,7 +2498,15 @@ "cancel_failed": "取消註銷失敗", "applied_at": "申請時間", "effective_at": "生效時間" - } + }, + "contact_email": "Contact email", + "email_code_placeholder": "Enter the 6-digit verification code", + "email_code_required": "Send and enter the email verification code first", + "email_code_resend": "Resend", + "email_code_sent": "Verification code sent", + "email_help_text": "Displayed publicly so others can contact you. This can differ from your registration email and may be left blank.", + "email_send_code": "Send verification code", + "email_send_code_countdown": "Resend in {{seconds}}s" }, "ads": { "label": "廣告", @@ -2399,5 +2530,8 @@ "title": "程式碼未通過完整性檢查", "help": "如為誤判,請透過網站 FAQ 中的「管理員聯絡方式」申請豁免。" } + }, + "announcements": { + "title": "Announcements" } }