mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
test(i18n): add user-visible localization ratchet
This commit is contained in:
@@ -12,7 +12,7 @@ ST-BME 的测试是 Node 回归测试(`tests/*.mjs`),`npm run test:stable`
|
|||||||
| 检索/召回 | `p0-regressions` 内相关、`trivial-user-input` | 召回、reroll 复用、注入 |
|
| 检索/召回 | `p0-regressions` 内相关、`trivial-user-input` | 召回、reroll 复用、注入 |
|
||||||
| 向量 | `vector-gate` / `vector-connection-probe` / `vector-sync-coalescer` | 向量门禁、连接探测、后台同步合并 |
|
| 向量 | `vector-gate` / `vector-connection-probe` / `vector-sync-coalescer` | 向量门禁、连接探测、后台同步合并 |
|
||||||
| Native | `native-layout-parity` / `native-rollout-matrix` | native/JS 一致性、灰度门控 |
|
| Native | `native-layout-parity` / `native-rollout-matrix` | native/JS 一致性、灰度门控 |
|
||||||
| 防线 | `index-slicing-ratchet` / `runtime-deps-completeness` | 见下 |
|
| 防线 | `index-slicing-ratchet` / `runtime-deps-completeness` / `i18n-user-visible-ratchet` | 见下 |
|
||||||
|
|
||||||
## 防线一:禁止切片 index.js(ratchet)
|
## 防线一:禁止切片 index.js(ratchet)
|
||||||
|
|
||||||
@@ -42,6 +42,14 @@ ST-BME 的测试是 Node 回归测试(`tests/*.mjs`),`npm run test:stable`
|
|||||||
|
|
||||||
详见 [`conventions.md`](conventions.md)。
|
详见 [`conventions.md`](conventions.md)。
|
||||||
|
|
||||||
|
## 防线三:前端 i18n 用户可见文案 ratchet
|
||||||
|
|
||||||
|
前端中英 i18n 只翻译 UI,不翻译 prompt、记忆节点、聊天内容或持久化数据。为了避免已迁移 UI 文件重新出现硬编码中文按钮/Toast/confirm,新增:
|
||||||
|
|
||||||
|
> `tests/i18n-user-visible-ratchet.mjs` 只扫描已迁移的 UI 文件,拦截明显用户可见模式:`toastr.*("中文")`、`confirm("中文")`、`textContent = "中文"`、`innerHTML` 中的中文按钮、`title/placeholder/aria-label` 中文等。
|
||||||
|
|
||||||
|
这不是全仓库中文禁令:中文文档、注释、测试 fixture、prompt/model/data 模块和 `i18n/zh-CN.js` 都不在这条规则里。新增用户可见文案时,应新增 catalog key,并通过 `t("...")` / `data-i18n` / keyed status 渲染。
|
||||||
|
|
||||||
## 重要测试文件
|
## 重要测试文件
|
||||||
|
|
||||||
- **`tests/p0-regressions.mjs`** — 主回归集合,覆盖提取、召回、恢复、UI 关键路径。
|
- **`tests/p0-regressions.mjs`** — 主回归集合,覆盖提取、召回、恢复、UI 关键路径。
|
||||||
@@ -50,6 +58,7 @@ ST-BME 的测试是 Node 回归测试(`tests/*.mjs`),`npm run test:stable`
|
|||||||
- **`tests/graph-persistence.mjs`** — 图谱持久化基础行为。
|
- **`tests/graph-persistence.mjs`** — 图谱持久化基础行为。
|
||||||
- **`tests/identity-resolver.mjs` / `tests/persistence-reducer.mjs`** — 身份解析核心、持久化 accepted/queued/pending 状态机。
|
- **`tests/identity-resolver.mjs` / `tests/persistence-reducer.mjs`** — 身份解析核心、持久化 accepted/queued/pending 状态机。
|
||||||
- **`tests/runtime-deps-completeness.mjs`** — 检查注入式控制器模块的 `runtime.X` 依赖均由对应 builder 提供。
|
- **`tests/runtime-deps-completeness.mjs`** — 检查注入式控制器模块的 `runtime.X` 依赖均由对应 builder 提供。
|
||||||
|
- **`tests/i18n-user-visible-ratchet.mjs`** — 检查已迁移 UI 文件不新增硬编码中文用户可见文案。
|
||||||
- **`tests/graph-snapshot-schema.mjs` / `tests/snapshot-forward-compat.mjs`** — 耐久快照契约、宽容解析和真实存储向前兼容往返。
|
- **`tests/graph-snapshot-schema.mjs` / `tests/snapshot-forward-compat.mjs`** — 耐久快照契约、宽容解析和真实存储向前兼容往返。
|
||||||
- **`tests/indexeddb-persistence.mjs`** — IndexedDB 快照、增量提交、hydrate。
|
- **`tests/indexeddb-persistence.mjs`** — IndexedDB 快照、增量提交、hydrate。
|
||||||
- **`tests/indexeddb-sync.mjs`** — 云端同步与冲突合并。
|
- **`tests/indexeddb-sync.mjs`** — 云端同步与冲突合并。
|
||||||
|
|||||||
@@ -4,6 +4,18 @@
|
|||||||
|
|
||||||
This page is split out from the [README](../../README.en.md) as the main ST-BME user configuration reference, preserving setting names, defaults, and tables for quick lookup by feature.
|
This page is split out from the [README](../../README.en.md) as the main ST-BME user configuration reference, preserving setting names, defaults, and tables for quick lookup by feature.
|
||||||
|
|
||||||
|
### Interface language
|
||||||
|
|
||||||
|
`Interface Language` only affects ST-BME frontend UI: the panel, menu entries, floating button, status messages, Toasts, recall cards, and graph system labels.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
- `Auto`: follows the SillyTavern / browser language when available, otherwise falls back to Chinese.
|
||||||
|
- `Simplified Chinese`: forces the Chinese UI.
|
||||||
|
- `English`: forces the English UI.
|
||||||
|
|
||||||
|
This setting **does not** translate chat content, user input, AI replies, memory nodes, recall injection text, or prompt construction. Switching the interface language does not change the memory graph or model behavior.
|
||||||
|
|
||||||
### Memory LLM
|
### Memory LLM
|
||||||
|
|
||||||
The memory LLM is used for:
|
The memory LLM is used for:
|
||||||
|
|||||||
@@ -4,6 +4,18 @@
|
|||||||
|
|
||||||
本文从 [README](../../README.md) 拆出 ST-BME 的主要用户配置说明,保留设置名称、默认值和表格,便于按功能查阅。
|
本文从 [README](../../README.md) 拆出 ST-BME 的主要用户配置说明,保留设置名称、默认值和表格,便于按功能查阅。
|
||||||
|
|
||||||
|
### 界面语言
|
||||||
|
|
||||||
|
`界面语言` 只影响 ST-BME 前端 UI:面板、菜单入口、悬浮按钮、状态、Toast、召回卡片和图谱系统标签。
|
||||||
|
|
||||||
|
可选值:
|
||||||
|
|
||||||
|
- `自动`:优先跟随 SillyTavern / 浏览器语言,识别不到时使用中文。
|
||||||
|
- `简体中文`:固定中文界面。
|
||||||
|
- `English`:固定英文界面。
|
||||||
|
|
||||||
|
这个设置**不会**翻译聊天内容、用户输入、AI 回复、记忆节点、召回注入文本或提示词构建。切换语言不会改变记忆图谱和模型行为。
|
||||||
|
|
||||||
### 记忆 LLM
|
### 记忆 LLM
|
||||||
|
|
||||||
记忆 LLM 用于:
|
记忆 LLM 用于:
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"test:i18n-boundary": "node tests/i18n-boundary.mjs",
|
"test:i18n-boundary": "node tests/i18n-boundary.mjs",
|
||||||
"test:i18n-status": "node tests/i18n-status.mjs",
|
"test:i18n-status": "node tests/i18n-status.mjs",
|
||||||
"test:ui-label-formatter": "node tests/ui-label-formatter.mjs",
|
"test:ui-label-formatter": "node tests/ui-label-formatter.mjs",
|
||||||
"test:i18n": "npm run test:i18n-catalog && npm run test:i18n-dom && npm run test:i18n-boundary && npm run test:i18n-status && npm run test:ui-label-formatter",
|
"test:i18n-ratchet": "node tests/i18n-user-visible-ratchet.mjs",
|
||||||
|
"test:i18n": "npm run test:i18n-catalog && npm run test:i18n-dom && npm run test:i18n-boundary && npm run test:i18n-status && npm run test:ui-label-formatter && npm run test:i18n-ratchet",
|
||||||
"bench:graph-layout": "node tests/perf/graph-layout-bench.mjs",
|
"bench:graph-layout": "node tests/perf/graph-layout-bench.mjs",
|
||||||
"bench:persist-delta": "node tests/perf/persist-delta-bench.mjs",
|
"bench:persist-delta": "node tests/perf/persist-delta-bench.mjs",
|
||||||
"bench:persist-load": "node tests/perf/persist-load-bench.mjs",
|
"bench:persist-load": "node tests/perf/persist-load-bench.mjs",
|
||||||
|
|||||||
332
tests/i18n-user-visible-ratchet.mjs
Normal file
332
tests/i18n-user-visible-ratchet.mjs
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
/**
|
||||||
|
* ST-BME: i18n user-visible ratchet test
|
||||||
|
*
|
||||||
|
* Scans migrated UI files for obvious hardcoded Chinese in user-visible
|
||||||
|
* API surface patterns (toastr, confirm, textContent, innerHTML, template
|
||||||
|
* literals, title, placeholder, aria-label, button HTML) and enforces that
|
||||||
|
* no new regressions are introduced. Comments and t("...") catalog lookups
|
||||||
|
* are explicitly allowed.
|
||||||
|
*
|
||||||
|
* This is a ratchet, not a global ban: it only checks the explicitly-listed
|
||||||
|
* UI files that have been migrated.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const PROJECT_ROOT = join(__dirname, "..");
|
||||||
|
|
||||||
|
// ─── Files to scan ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const UI_JS_FILES = [
|
||||||
|
"ui/panel-bridge.js",
|
||||||
|
"ui/recall-message-ui.js",
|
||||||
|
"ui/panel-ena-sections.js",
|
||||||
|
"ui/ui-status.js",
|
||||||
|
"ui/history-notice.js",
|
||||||
|
"ui/ui-label-formatter.js",
|
||||||
|
"ui/graph-renderer.js",
|
||||||
|
];
|
||||||
|
|
||||||
|
const UI_HTML_FILES = [
|
||||||
|
"ui/panel.html",
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Pattern definitions ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect CJK Han characters in a string.
|
||||||
|
*/
|
||||||
|
const CJK_RE = /\p{Script=Han}/u;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip JS block and single-line comments from source.
|
||||||
|
* Conservative: removes comment content so Chinese inside comments is not flagged.
|
||||||
|
*/
|
||||||
|
function stripComments(src) {
|
||||||
|
let result = src.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||||
|
result = result.replace(/\/\/.*$/gm, "");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip t("...") and t('...') catalog call patterns so Chinese inside
|
||||||
|
* i18n lookups is not flagged. Also handles t("key", { params }, { opts }).
|
||||||
|
* Neutralises the string content so CJK regex won't match inside t() args.
|
||||||
|
*/
|
||||||
|
function stripI18nCalls(src) {
|
||||||
|
let result = src;
|
||||||
|
// Double-quoted t() calls: t("anything", {...}, {...})
|
||||||
|
result = result.replace(
|
||||||
|
/\bt\s*\(\s*"([^"\\]|\\.)*"\s*(?:,\s*\{[^}]*\}\s*)*(?:,\s*\{[^}]*\}\s*)*\)/g,
|
||||||
|
(m) => m.replace(/"/g, "\x00"),
|
||||||
|
);
|
||||||
|
// Single-quoted t() calls: t('anything', {...}, {...})
|
||||||
|
result = result.replace(
|
||||||
|
/\bt\s*\(\s*'([^'\\]|\\.)*'\s*(?:,\s*\{[^}]*\}\s*)*(?:,\s*\{[^}]*\}\s*)*\)/g,
|
||||||
|
(m) => m.replace(/'/g, "\x00"),
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known violations baseline: counts of Chinese-containing lines per file
|
||||||
|
* AFTER comments and t() calls are stripped. These represent internal
|
||||||
|
* defaults / fallback labels that route through i18n at runtime.
|
||||||
|
* The ratchet ensures these counts do not increase.
|
||||||
|
*/
|
||||||
|
const BASELINE = {
|
||||||
|
"ui/panel-bridge.js": 7, // console.error/warn messages (dev-only, not user-visible)
|
||||||
|
"ui/recall-message-ui.js": 0, // fully migrated
|
||||||
|
"ui/panel-ena-sections.js": 0, // fully migrated
|
||||||
|
"ui/ui-status.js": 0, // fully migrated
|
||||||
|
"ui/history-notice.js": 0, // fully migrated
|
||||||
|
"ui/ui-label-formatter.js": 0, // fully migrated
|
||||||
|
"ui/graph-renderer.js": 0, // fully migrated
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Scanning logic ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function scanFileForUserVisibleChinese(relativePath) {
|
||||||
|
const fullPath = join(PROJECT_ROOT, relativePath);
|
||||||
|
let source;
|
||||||
|
try {
|
||||||
|
source = readFileSync(fullPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
console.warn(`[i18n-ratchet] SKIP — file not found: ${relativePath}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripped = stripComments(source);
|
||||||
|
const safe = stripI18nCalls(stripped);
|
||||||
|
const lines = safe.split("\n");
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (!CJK_RE.test(line)) continue;
|
||||||
|
|
||||||
|
// Skip import lines
|
||||||
|
if (/^\s*import\s/.test(line)) continue;
|
||||||
|
|
||||||
|
// Skip console/debugLog calls (dev-only, not user-visible)
|
||||||
|
if (/console\.(log|error|warn|info|debug)\s*\(/.test(line)) continue;
|
||||||
|
if (/debugLog\s*\(/.test(line)) continue;
|
||||||
|
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
// ─── User-visible API patterns that SHOULD use t() ────
|
||||||
|
|
||||||
|
// 1. toastr.*() calls with Chinese
|
||||||
|
if (/\btoastr\s*\.\s*(?:success|error|info|warning|notify)\s*\(/.test(trimmed) && CJK_RE.test(line)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "toastr", snippet: trimmed.slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. confirm(...) calls with Chinese
|
||||||
|
if (/\bconfirm\s*\(/.test(trimmed) && CJK_RE.test(line)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "confirm", snippet: trimmed.slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. prompt(...) calls with Chinese
|
||||||
|
if (/\bprompt\s*\(/.test(trimmed) && CJK_RE.test(line)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "prompt", snippet: trimmed.slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. .textContent = with Chinese
|
||||||
|
if (/\.textContent\s*=/.test(trimmed) && CJK_RE.test(line)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "textContent", snippet: trimmed.slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. .innerHTML = with Chinese
|
||||||
|
if (/\.innerHTML\s*=/.test(trimmed) && CJK_RE.test(line)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "innerHTML", snippet: trimmed.slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. .title / .placeholder / .ariaLabel = with Chinese
|
||||||
|
if (/\.(?:title|placeholder|ariaLabel)\s*=/.test(trimmed) && CJK_RE.test(line)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "attribute", snippet: trimmed.slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. setAttribute('title'/'placeholder'/'aria-label', ...) with Chinese
|
||||||
|
if (/\.setAttribute\s*\(\s*["'](?:title|placeholder|aria-label)["']\s*,/.test(trimmed) && CJK_RE.test(line)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "setAttribute", snippet: trimmed.slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. <button> with Chinese text
|
||||||
|
if (/<button[^>]*>.*\p{Script=Han}.*<\/button>/u.test(trimmed)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "button-html", snippet: trimmed.slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Template literals with Chinese in innerHTML/textContent context
|
||||||
|
if (/`[^`]*\p{Script=Han}[^`]*`/u.test(trimmed)) {
|
||||||
|
if (/\.innerHTML\s*[+=]|\.textContent\s*[+=]|\.innerText\s*[+=]/.test(trimmed)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "template-literal", snippet: trimmed.slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanHtmlForMissingDataI18n(relativePath) {
|
||||||
|
const fullPath = join(PROJECT_ROOT, relativePath);
|
||||||
|
let source;
|
||||||
|
try {
|
||||||
|
source = readFileSync(fullPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
console.warn(`[i18n-ratchet] SKIP — file not found: ${relativePath}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const violations = [];
|
||||||
|
const lines = source.split("\n");
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (!CJK_RE.test(line)) continue;
|
||||||
|
|
||||||
|
// Skip lines that already have data-i18n (properly migrated)
|
||||||
|
if (/data-i18n(?:-[a-z-]+)?=/.test(line)) continue;
|
||||||
|
|
||||||
|
// Skip HTML comments
|
||||||
|
if (/^\s*<!--/.test(line.trim())) continue;
|
||||||
|
|
||||||
|
// <button...>Chinese text</button> without data-i18n
|
||||||
|
if (/<button[^>]*>.*\p{Script=Han}.*<\/button>/u.test(line)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "button-html", snippet: line.trim().slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// title="Chinese", placeholder="Chinese", aria-label="Chinese"
|
||||||
|
if (/(?:title|placeholder|aria-label)\s*=\s*["'][^"']*\p{Script=Han}[^"']*["']/u.test(line)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "attribute-chinese", snippet: line.trim().slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <span>Chinese</span> without data-i18n
|
||||||
|
if (/<span[^>]*>[\s]*\p{Script=Han}[\p{Script=Han}\s\w]*<\/span>/u.test(line) && !/data-i18n/.test(line)) {
|
||||||
|
violations.push({ file: relativePath, line: i + 1, kind: "span-chinese-no-i18n", snippet: line.trim().slice(0, 120) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Count broad Chinese lines for ratchet ──────────────────────────────────
|
||||||
|
|
||||||
|
function countChineseLines(relativePath) {
|
||||||
|
const fullPath = join(PROJECT_ROOT, relativePath);
|
||||||
|
let source;
|
||||||
|
try {
|
||||||
|
source = readFileSync(fullPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const stripped = stripComments(source);
|
||||||
|
const safe = stripI18nCalls(stripped);
|
||||||
|
const lines = safe.split("\n");
|
||||||
|
let count = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (CJK_RE.test(line)) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Test execution ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const allViolations = [];
|
||||||
|
let totalFilesScanned = 0;
|
||||||
|
|
||||||
|
for (const relPath of UI_JS_FILES) {
|
||||||
|
const violations = scanFileForUserVisibleChinese(relPath);
|
||||||
|
allViolations.push(...violations);
|
||||||
|
totalFilesScanned++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const relPath of UI_HTML_FILES) {
|
||||||
|
const violations = scanHtmlForMissingDataI18n(relPath);
|
||||||
|
allViolations.push(...violations);
|
||||||
|
totalFilesScanned++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group violations by file for reporting
|
||||||
|
const violationsByFile = new Map();
|
||||||
|
for (const v of allViolations) {
|
||||||
|
if (!violationsByFile.has(v.file)) violationsByFile.set(v.file, []);
|
||||||
|
violationsByFile.get(v.file).push(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\ni18n user-visible ratchet: scanned ${totalFilesScanned} files`);
|
||||||
|
console.log(` Total user-visible Chinese violations found: ${allViolations.length}`);
|
||||||
|
|
||||||
|
if (violationsByFile.size > 0) {
|
||||||
|
for (const [file, vs] of violationsByFile) {
|
||||||
|
console.log(`\n ${file}:`);
|
||||||
|
for (const v of vs) {
|
||||||
|
console.log(` L${v.line} [${v.kind}]: ${v.snippet}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broad Chinese line counts (comments + t() calls stripped)
|
||||||
|
console.log(`\nBroad Chinese line counts (comments + t() calls stripped):`);
|
||||||
|
for (const relPath of UI_JS_FILES) {
|
||||||
|
const count = countChineseLines(relPath);
|
||||||
|
if (count >= 0) {
|
||||||
|
console.log(` ${relPath}: ${count}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Assertions ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// 1. Strict user-visible violations: MUST be zero
|
||||||
|
// toastr, confirm, prompt, textContent, innerHTML, attribute, setAttribute,
|
||||||
|
// button-html, template-literal patterns must use t() lookups.
|
||||||
|
const strictKinds = [
|
||||||
|
"toastr", "confirm", "prompt", "textContent", "innerHTML",
|
||||||
|
"attribute", "setAttribute", "button-html", "template-literal",
|
||||||
|
];
|
||||||
|
|
||||||
|
const strictViolations = allViolations.filter((v) => strictKinds.includes(v.kind));
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
strictViolations.length,
|
||||||
|
0,
|
||||||
|
`Strict i18n violation(s) found (toastr/confirm/prompt/textContent/innerHTML/attribute/setAttribute/button-html/template-literal must use t()):\n` +
|
||||||
|
strictViolations.map((v) => ` ${v.file}:L${v.line} [${v.kind}]: ${v.snippet}`).join("\n") +
|
||||||
|
"\n\nReplace hardcoded Chinese with t() catalog lookups.",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Ratchet: total Chinese lines per file must not exceed baseline
|
||||||
|
for (const [file, baseline] of Object.entries(BASELINE)) {
|
||||||
|
const current = countChineseLines(file);
|
||||||
|
if (current < 0) continue; // file was skipped
|
||||||
|
assert.ok(
|
||||||
|
current <= baseline,
|
||||||
|
`i18n ratchet violated for ${file}: found ${current} Chinese-containing lines (comments/t() stripped), baseline is ${baseline}. ` +
|
||||||
|
`New hardcoded Chinese must use t() lookups instead.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. panel.html: HTML Chinese content without data-i18n must not grow beyond baseline
|
||||||
|
const htmlViolations = allViolations.filter((v) => UI_HTML_FILES.includes(v.file));
|
||||||
|
const HTML_BASELINE = 441; // panel.html has substantial unmigrated Chinese text
|
||||||
|
assert.ok(
|
||||||
|
htmlViolations.length <= HTML_BASELINE,
|
||||||
|
`panel.html has ${htmlViolations.length} Chinese content lines without data-i18n, expected ≤ ${HTML_BASELINE} (existing baseline):\n` +
|
||||||
|
htmlViolations.map((v) => ` L${v.line} [${v.kind}]: ${v.snippet}`).join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("\ni18n user-visible ratchet tests passed");
|
||||||
@@ -978,7 +978,7 @@ export class GraphRenderer {
|
|||||||
(hasRight ? splitX : W) - pad * 2 - (hasRight ? gutter / 2 : 0),
|
(hasRight ? splitX : W) - pad * 2 - (hasRight ? gutter / 2 : 0),
|
||||||
),
|
),
|
||||||
h: Math.max(0, H - pad * 2 - 6),
|
h: Math.max(0, H - pad * 2 - 6),
|
||||||
label: '客观层',
|
label: 'Objective Layer',
|
||||||
labelKey: 'graph.scope.objective',
|
labelKey: 'graph.scope.objective',
|
||||||
tint: 'rgba(26, 35, 50, 0.42)',
|
tint: 'rgba(26, 35, 50, 0.42)',
|
||||||
key: 'objective',
|
key: 'objective',
|
||||||
@@ -1013,7 +1013,7 @@ export class GraphRenderer {
|
|||||||
y: yTop,
|
y: yTop,
|
||||||
w: rightW,
|
w: rightW,
|
||||||
h: fullH,
|
h: fullH,
|
||||||
label: '用户 POV',
|
label: 'User POV',
|
||||||
labelKey: 'graph.scope.userPov',
|
labelKey: 'graph.scope.userPov',
|
||||||
tint: 'rgba(32, 48, 40, 0.42)',
|
tint: 'rgba(32, 48, 40, 0.42)',
|
||||||
key: 'user',
|
key: 'user',
|
||||||
@@ -1048,7 +1048,7 @@ export class GraphRenderer {
|
|||||||
y: yc,
|
y: yc,
|
||||||
w: rightW,
|
w: rightW,
|
||||||
h: ph,
|
h: ph,
|
||||||
label: `角色 POV · ${displayName}`,
|
label: `Character POV · ${displayName}`,
|
||||||
labelKey: 'graph.scope.characterPov',
|
labelKey: 'graph.scope.characterPov',
|
||||||
labelParams: { name: displayName },
|
labelParams: { name: displayName },
|
||||||
tint: 'rgba(55, 42, 28, 0.38)',
|
tint: 'rgba(55, 42, 28, 0.38)',
|
||||||
@@ -1071,7 +1071,7 @@ export class GraphRenderer {
|
|||||||
y: uy,
|
y: uy,
|
||||||
w: rightW,
|
w: rightW,
|
||||||
h: userStripH,
|
h: userStripH,
|
||||||
label: '用户 POV',
|
label: 'User POV',
|
||||||
labelKey: 'graph.scope.userPov',
|
labelKey: 'graph.scope.userPov',
|
||||||
tint: 'rgba(32, 48, 40, 0.42)',
|
tint: 'rgba(32, 48, 40, 0.42)',
|
||||||
key: 'user',
|
key: 'user',
|
||||||
@@ -1100,12 +1100,12 @@ export class GraphRenderer {
|
|||||||
});
|
});
|
||||||
const panels = [];
|
const panels = [];
|
||||||
const objectiveRect = makeRect(width * 0.5, height * 0.5, width * 0.82, height * 0.78);
|
const objectiveRect = makeRect(width * 0.5, height * 0.5, width * 0.82, height * 0.78);
|
||||||
panels.push({ ...objectiveRect, label: '客观层', labelKey: 'graph.scope.objective', tint: 'rgba(87, 199, 255, 0.02)', key: 'objective' });
|
panels.push({ ...objectiveRect, label: 'Objective Layer', labelKey: 'graph.scope.objective', tint: 'rgba(87, 199, 255, 0.02)', key: 'objective' });
|
||||||
for (const n of objective) n.regionRect = objectiveRect;
|
for (const n of objective) n.regionRect = objectiveRect;
|
||||||
|
|
||||||
if (userPov.length) {
|
if (userPov.length) {
|
||||||
const userRect = makeRect(width * 0.68, height * 0.68, width * 0.44, height * 0.42);
|
const userRect = makeRect(width * 0.68, height * 0.68, width * 0.44, height * 0.42);
|
||||||
panels.push({ ...userRect, label: '用户 POV', labelKey: 'graph.scope.userPov', tint: 'rgba(125, 255, 155, 0.02)', key: 'user' });
|
panels.push({ ...userRect, label: 'User POV', labelKey: 'graph.scope.userPov', tint: 'rgba(125, 255, 155, 0.02)', key: 'user' });
|
||||||
for (const n of userPov) n.regionRect = userRect;
|
for (const n of userPov) n.regionRect = userRect;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1121,7 +1121,7 @@ export class GraphRenderer {
|
|||||||
const rect = makeRect(cx, cy, width * 0.34, height * 0.36);
|
const rect = makeRect(cx, cy, width * 0.34, height * 0.36);
|
||||||
const key = `char:${owner || 'unknown'}`;
|
const key = `char:${owner || 'unknown'}`;
|
||||||
const displayName = owner || translateUi('graph.scope.unknownCharacter');
|
const displayName = owner || translateUi('graph.scope.unknownCharacter');
|
||||||
panels.push({ ...rect, label: `角色 POV · ${displayName}`, labelKey: 'graph.scope.characterPov', labelParams: { name: displayName }, tint: 'rgba(255, 179, 71, 0.02)', key });
|
panels.push({ ...rect, label: `Character POV · ${displayName}`, labelKey: 'graph.scope.characterPov', labelParams: { name: displayName }, tint: 'rgba(255, 179, 71, 0.02)', key });
|
||||||
for (const n of nodes) n.regionRect = rect;
|
for (const n of nodes) n.regionRect = rect;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14342,7 +14342,7 @@ function _refreshRuntimeStatus() {
|
|||||||
const upgradeState = graphPersistence.authorityUpgradeState || {};
|
const upgradeState = graphPersistence.authorityUpgradeState || {};
|
||||||
const text = formatUiStatusText(runtimeStatus) || t("status.idle");
|
const text = formatUiStatusText(runtimeStatus) || t("status.idle");
|
||||||
const meta = formatUiStatusMeta(runtimeStatus) || t("status.initial.runtime.detail");
|
const meta = formatUiStatusMeta(runtimeStatus) || t("status.initial.runtime.detail");
|
||||||
const upgradeText = upgradeState.text || graphPersistence.authorityUpgradeText || "";
|
const upgradeText = formatUiStatusText(upgradeState) || graphPersistence.authorityUpgradeText || "";
|
||||||
const displayMeta = upgradeText ? `${meta} · ${upgradeText}` : meta;
|
const displayMeta = upgradeText ? `${meta} · ${upgradeText}` : meta;
|
||||||
_setText("bme-status-text", text);
|
_setText("bme-status-text", text);
|
||||||
_setText("bme-status-meta", displayMeta);
|
_setText("bme-status-meta", displayMeta);
|
||||||
|
|||||||
@@ -21,15 +21,23 @@ export const BATCH_STAGE_SEVERITY = {
|
|||||||
// UI 状态工厂
|
// UI 状态工厂
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export function createUiStatus(text = "待命", meta = "", level = "idle") {
|
export function createUiStatus(text = "", meta = "", level = "idle") {
|
||||||
if (text && typeof text === "object") {
|
if (text && typeof text === "object") {
|
||||||
return createI18nStatus({
|
return createI18nStatus({
|
||||||
...(text || {}),
|
...(text || {}),
|
||||||
level: text.level || level || "idle",
|
level: text.level || level || "idle",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!text && !meta) {
|
||||||
|
return createI18nStatus({
|
||||||
|
textKey: "status.idle",
|
||||||
|
textFallback: "Idle",
|
||||||
|
metaFallback: "",
|
||||||
|
level,
|
||||||
|
});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
text: String(text || "待命"),
|
text: String(text || "Idle"),
|
||||||
meta: String(meta || ""),
|
meta: String(meta || ""),
|
||||||
level,
|
level,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
@@ -40,7 +48,7 @@ export function createGraphPersistenceState() {
|
|||||||
return {
|
return {
|
||||||
loadState: "no-chat",
|
loadState: "no-chat",
|
||||||
chatId: "",
|
chatId: "",
|
||||||
reason: "当前尚未进入聊天",
|
reason: t("status.noChat", {}, { fallback: "No chat is currently open" }),
|
||||||
attemptIndex: 0,
|
attemptIndex: 0,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
lastPersistedRevision: 0,
|
lastPersistedRevision: 0,
|
||||||
@@ -153,8 +161,8 @@ export function createGraphPersistenceState() {
|
|||||||
authorityDegradedReason: "",
|
authorityDegradedReason: "",
|
||||||
authorityUpgradeState: createAuthorityUpgradeState(),
|
authorityUpgradeState: createAuthorityUpgradeState(),
|
||||||
authorityUpgradeMode: "standalone",
|
authorityUpgradeMode: "standalone",
|
||||||
authorityUpgradeText: "纯前端模式",
|
authorityUpgradeText: "Frontend-only mode",
|
||||||
authorityUpgradeMeta: "未检测到可用服务端增强,BME 将继续本地运行",
|
authorityUpgradeMeta: "No server-side enhancement detected; BME will continue running locally",
|
||||||
authorityUpgradeLevel: "idle",
|
authorityUpgradeLevel: "idle",
|
||||||
authorityUpgradeReady: false,
|
authorityUpgradeReady: false,
|
||||||
authorityMigrationState: "idle",
|
authorityMigrationState: "idle",
|
||||||
|
|||||||
Reference in New Issue
Block a user