style(copy): punctuation rules and site-wide fix with a CI check (QA U11)

- glossary: 10 punctuation rules (full-width in Chinese, CJK/Latin spacing,
  full-width parens around Chinese, single ellipsis, 「」 quotes, period rules)
- scripts/check-copy-punctuation.mjs: checks string literals, templates and JSX
  text (not comments, types, keys, match/compare strings); --fix, --strict,
  --by-file; wired into deno task ci as check:copy in warn mode
- --fix applied to web, server and contracts; files other lanes were editing
  (subscriptions, sites, browser, agent, auth/account, inventory) left for them
- tests updated for the changed messages

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 20:09:29 +08:00
1 parent 9b017906cf
commit 52402864d6
143 files changed
+985 -606

No files matched your search

@@ -259,7 +259,7 @@ export function EntityPicker({
</ul>
{!hits.local.length && !hits.tmdb.length && !search.isFetching ? (
<p className="px-2 py-1.5 text-meta text-muted-foreground">
没有匹配的作品{hits.tmdbConfigured ? "" : ";TMDB 未配置,只搜了本地"}
没有匹配的作品{hits.tmdbConfigured ? "" : ";TMDB 未配置,只搜了本地"}
</p>
) : null}
</>
@@ -56,7 +56,7 @@ export function rowProblem(r: WizardRow): string | null {
if (r.excluded) return null;
if (!r.entity) return "未选择作品";
const eps = parseEpisodes(r.episodes);
if (eps === null) return "集号格式不对,如 3、3-5、3,4";
if (eps === null) return "集号格式不对,如 3、3-5、3,4";
if (r.season.trim() && !/^\d{1,4}$/.test(r.season.trim())) return "季号必须是数字";
if (!isMovieKind(r.entity.kind) && !r.season.trim()) return "剧集需要季号";
return null;
@@ -98,7 +98,7 @@ export function WizardBulkBar({
value={offset}
onChange={(e) => setOffset(e.target.value)}
placeholder="±N"
aria-label="集号偏移量(可为负)"
aria-label="集号偏移量(可为负)"
className="h-8 w-16 text-center"
/>
<Button
@@ -131,7 +131,7 @@ export function WizardBulkBar({
)}
{stats.excluded ? (
<StatusBadge>
{stats.excluded} 项已排除{stats.bySize ? `(${stats.bySize} 项低于最小体积)` : ""}
{stats.excluded} 项已排除{stats.bySize ? `(${stats.bySize} 项低于最小体积)` : ""}
</StatusBadge>
) : null}
</div>
@@ -270,7 +270,7 @@ export function StepFiles(p: StepFilesProps) {
)}
{p.truncated ? (
<p className="rounded-lg border border-warning/35 bg-warning/10 px-3 py-1.5 text-meta text-warning">
目录里视频超过 200 个,只识别了前 200 个。进入子目录分批整理。
目录里视频超过 200 个,只识别了前 200 个。进入子目录分批整理。
</p>
) : null}
@@ -86,8 +86,8 @@ export function StepOptions({
if (r.mediaKind === "mixed" || r.mediaKind === "other" || !r.mediaKind) return null;
const hasMovie = [...kinds].some((k) => k === "movie");
const hasShow = [...kinds].some((k) => k !== "movie");
if (r.mediaKind === "movie" && hasShow) return "电影库,本次含剧集";
if (r.mediaKind !== "movie" && hasMovie) return "剧集库,本次含电影";
if (r.mediaKind === "movie" && hasShow) return "电影库,本次含剧集";
if (r.mediaKind !== "movie" && hasMovie) return "剧集库,本次含电影";
return null;
};
const t = options.target;
@@ -100,14 +100,14 @@ export function StepOptions({
checked={options.action === "copy"}
onSelect={() => set({ action: "copy" })}
title="复制"
hint="保留源文件,不影响做种"
hint="保留源文件,不影响做种"
/>
<Choice
checked={options.action === "move"}
onSelect={() => set({ action: "move" })}
disabled={hnrCount ? `${hnrCount} 项做种中且 H&R 未满足,不能移动` : null}
disabled={hnrCount ? `${hnrCount} 项做种中且 H&R 未满足,不能移动` : null}
title="移动"
hint="入库后删源;跨卷时按复制 → 核验 → 删源"
hint="入库后删源;跨卷时按复制 → 核验 → 删源"
/>
<Choice checked={false} onSelect={() => {}} disabled="还不支持硬链接" title="硬链接" />
</div>
@@ -119,19 +119,19 @@ export function StepOptions({
checked={options.onExisting === "conflict"}
onSelect={() => set({ onExisting: "conflict" })}
title="交给待处理"
hint="该组不执行,冲突进待处理"
hint="该组不执行,冲突进待处理"
/>
<Choice
checked={options.onExisting === "skip"}
onSelect={() => set({ onExisting: "skip" })}
title="跳过"
hint="只跳过已存在的文件,其余照常整理"
hint="只跳过已存在的文件,其余照常整理"
/>
<Choice
checked={options.onExisting === "replace_with_verify"}
onSelect={() => set({ onExisting: "replace_with_verify" })}
title="替换"
hint="新文件核验通过后,旧文件移入回收站"
hint="新文件核验通过后,旧文件移入回收站"
/>
</div>
</Section>
@@ -144,7 +144,7 @@ export function StepOptions({
checked={t.mode === "auto"}
onSelect={() => set({ target: { mode: "auto" } })}
title="自动"
hint="已绑定的作品沿用原目录,否则按作品类型选库根"
hint="已绑定的作品沿用原目录,否则按作品类型选库根"
/>
{libRoots.map((r) => (
<Choice
@@ -165,7 +165,7 @@ export function StepOptions({
checked={t.mode === "dir"}
onSelect={() => set({ target: { mode: "dir", ref: t.mode === "dir" ? t.ref : { root: "", path: "" } } })}
title="指定目录"
hint="文件按模板命名后直接放进这个目录,不建路径绑定"
hint="文件按模板命名后直接放进这个目录,不建路径绑定"
/>
{t.mode === "dir" ? (
<DirPicker
@@ -186,7 +186,7 @@ export function StepOptions({
title="按命名模板"
hint={
<>
模板按 作品覆盖 → 库根 → 全局 取第一个,预览里显示实际来源。
模板按 作品覆盖 → 库根 → 全局 取第一个,预览里显示实际来源。
<Link to="/console/settings#library/naming" className="ml-1 underline underline-offset-2">
去设置
</Link>
@@ -206,7 +206,7 @@ export function StepOptions({
<div className="flex items-start justify-between gap-3 rounded-xl border border-border px-3 py-2">
<span className="text-body">
刷新元数据
<span className="block text-meta text-muted-foreground">按字段刷新,锁定的字段不变</span>
<span className="block text-meta text-muted-foreground">按字段刷新,锁定的字段不变</span>
</span>
<Switch
checked={options.refreshMetadata}
@@ -214,7 +214,7 @@ export function StepOptions({
aria-label="入库后刷新元数据"
/>
</div>
<p className="text-meta text-muted-foreground">同名字幕、NFO、图片随主文件一起处理,预览里列出。</p>
<p className="text-meta text-muted-foreground">同名字幕、NFO、图片随主文件一起处理,预览里列出。</p>
</Section>
</div>
</div>
@@ -27,10 +27,10 @@ import { PathSegments } from "./path-segments.tsx";
export type GroupTitle = (g: FilesOrganizeGroup) => string;
const ERROR_TEXT: Record<string, string> = {
plan_blocked: "目标已存在,按“交给待处理”不执行",
plan_blocked: "目标已存在,按「交给待处理」不执行",
plan_stale: "源文件在预览后有变化",
approval_required: "移动需要审批或持续授权",
download_active: "做种中且 H&R 未满足,不能移动",
download_active: "做种中且 H&R 未满足,不能移动",
};
/** 服务端原因码(message 以 code: 开头)→ 文案 */
@@ -187,7 +187,7 @@ export function PreviewGroupCard({
{result && record ? `${record.counts.succeeded}/${record.counts.total} 成功 · ` : ""}
{g.files.length} 个文件 · {formatBytes(size, 1)}
{g.action ? ` · ${g.action === "move" ? "移动" : "复制"}` : ""}
{g.naming ? ` · 命名:${TEMPLATE_SOURCE[g.naming.templateSource] ?? g.naming.templateSource}` : ""}
{g.naming ? ` · 命名:${TEMPLATE_SOURCE[g.naming.templateSource] ?? g.naming.templateSource}` : ""}
</span>
{result && g.recordId ? (
<Link className="ml-auto text-meta underline underline-offset-2" to={`/console/organize/${g.recordId}`}>
@@ -210,7 +210,7 @@ export function PreviewGroupCard({
</p>
))}
{!result ? <TargetLine g={g} roots={roots} /> : null}
{g.truncated ? <p className="text-caption text-warning">文件过多,预览只列出前一部分。</p> : null}
{g.truncated ? <p className="text-caption text-warning">文件过多,预览只列出前一部分。</p> : null}
{!result && nested.length ? (
<ul className="divide-y divide-border/50">
{nested.map((n) => (
@@ -227,7 +227,7 @@ export function PreviewGroupCard({
) : null}
{!result && g.excluded?.length ? (
<p className="text-meta text-muted-foreground">
排除 {g.excluded.length} 个:
排除 {g.excluded.length} 个:
{g.excluded
.slice(0, 6)
.map((e) => `${baseName(e.source)}(${EXCLUDE_REASON[e.reason] ?? e.reason})`)
@@ -258,7 +258,9 @@ export function PreviewList({
const shown = onlyProblems ? groups.filter(groupHasProblem) : groups;
const pg = pageOf(shown, page);
if (!shown.length)
return <p className="py-8 text-center text-caption text-muted-foreground">没有问题项。关闭“只看问题”查看全部。</p>;
return (
<p className="py-8 text-center text-caption text-muted-foreground">没有问题项。关闭「只看问题」查看全部。</p>
);
return (
<div className="space-y-3">
{pg.items.map((g, i) => (