Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,32 @@ src-tauri/target/
*.log
*.tmp

# Local npm cache & scratch dirs (setup-time artifacts)
.npm-cache/
.tmp-engine-check/

# Local FFmpeg dev package (for ffmpeg-sys-next builds; not part of the repo)
ffmpeg-8.1.2-full_build-shared/
ffmpeg-*.7z

# FFmpeg shared-library DLLs for Windows NSIS builds (too large for git;
# see README "Windows release builds (NSIS)" for how to obtain them)
src-tauri/resources/

# Local toolchain scratch dirs (machine-specific, not part of the repo)
.cargo/
.llvm-bin/
.llvm-pkg2/
llvm-installer.exe
whisper-build.log

# WorkBuddy project data (local-only, not part of the repo)
.workbuddy/

# Python bytecode
__pycache__/
*.pyc

# Scratch/experimental files
scratch/

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ Download pre-built binaries from [Latest Releases](https://github.com/AIEraDev/C
- Rust 1.70+ (`rustup`)
- FFmpeg 6.0+ with development headers

> **Windows release builds (NSIS)**: building the installer requires the FFmpeg
> shared-library DLLs to be present under `src-tauri/resources/` (they are too
> large for git, see `.gitignore`). Copy them from any FFmpeg `full-shared`
> build (e.g. gyan.dev FFmpeg 8.1.2):
>
> ```text
> avcodec-62.dll avdevice-62.dll avfilter-11.dll avformat-62.dll
> avutil-60.dll swresample-6.dll swscale-9.dll
> ```
>
> The DLLs must match the FFmpeg version used by `src-tauri/bin/ffmpeg-x86_64-pc-windows-msvc.exe`.
> `npm run tauri build` copies them next to the main executable via the
> `resources` map in `tauri.conf.json`.

### Quick Start

```bash
Expand Down
26 changes: 5 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@
"mobile:live": "CAPACITOR_LIVE_RELOAD=true npx cap sync"
},
"dependencies": {
"@asamuzakjp/css-color": "^5.1.11",
"@capacitor/core": "^8.4.0",
"@capacitor/filesystem": "^8.1.2",
"@capacitor/share": "^8.0.1",
"@clypra-studio/engine": "1.3.0",
"@clypra-studio/engine": "^1.4.0",
"@clypra-studio/shaders": "0.1.5",
"@clypra/ui-color-picker": "0.2.1",
"@fontsource-variable/dancing-script": "^5.2.8",
Expand Down Expand Up @@ -73,7 +74,6 @@
"@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"canvas": "^3.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
82 changes: 82 additions & 0 deletions scripts/analyze-i18n.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// i18n 覆盖分析 v3 —— 用与生成脚本一致的 key 提取,输出确定缺失的 UI 字符串
const fs = require("fs");
const path = require("path");

const root = "H:/AI/Clypra";
const provider = fs.readFileSync(path.join(root, "src/i18n/I18nProvider.tsx"), "utf8");

function extractKeys(block) {
const keys = new Set();
const re = /"((?:[^"\\]|\\.)*)"\s*:\s*"/g;
let m;
while ((m = re.exec(block))) keys.add(m[1]);
return keys;
}
const zhTwBlock = provider.slice(provider.indexOf("const ZH_TW"), provider.indexOf("const ZH_CN"));
const zhCnBlock = provider.slice(provider.indexOf("const ZH_CN"), provider.indexOf("const EN_FROM_ZH_TW"));
const keySet = new Set([...extractKeys(zhTwBlock), ...extractKeys(zhCnBlock)]);
console.log("字典 key 总数:", keySet.size);

function walk(dir, out = []) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) {
if (["node_modules", "__tests__", "i18n", "workers"].includes(e.name)) continue;
walk(p, out);
} else if (/\.tsx?$/.test(e.name)) out.push(p);
}
return out;
}

const files = walk(path.join(root, "src"));
const englishish = /^[A-Za-z][A-Za-z0-9 ,.'"!?%&/():;+*\-–—…~•◇●✂×|·<>$#@_]{2,}$/;
const tailwindish = /^(absolute|relative|fixed|sticky|static|flex|grid|block|inline|hidden|visible|opacity|animate|bg-|text-|border|rounded|shadow|ring|outline|w-|h-|min-w|max-w|min-h|max-h|p-\d|px-|py-|pt-|pr-|pb-|pl-|m-\d|mx-|my-|mt-|mr-|mb-|ml-|gap-|top-|left-|right-|bottom-|inset|object-|overflow|transition|pointer-events|select-none|cursor-|hover:|focus:|active:|group-|z-\d|space-|divide-|backdrop-|transform|origin-|scale-|rotate-|translate-|skew-|duration-|ease-|delay-|fill-|stroke-|blur-|brightness-|contrast-|grayscale-|invert-|saturate-|sepia-|hue-rotate-|drop-shadow-|font-|leading-|tracking-|italic|underline|line-through|uppercase|lowercase|capitalize|normal-|whitespace-|break-|truncate|list-|columns-|auto-|grid-cols|col-|row-|self-|justify-|items-|content-|place-|order-|flex-|basis-|grow|shrink|aspect-)/;

const candidates = new Map(); // str -> Set(locations)
const push = (s, loc) => {
if (!candidates.has(s)) candidates.set(s, new Set());
candidates.get(s).add(loc);
};

for (const f of files) {
const content = fs.readFileSync(f, "utf8");
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 排除纯代码行(含运算符/引号较多的模板字符串)
if (/[=<>{}()\[\]]/.test(line) && !/>\s*[^<>{}]*[A-Za-z]{2,}[^<>{}]*</.test(line)) {
// 仍扫描 title/aria-label 属性
const propRe = /\b(title|placeholder|aria-label)\s*=\s*["'`]([^"'`]{3,90})["'`]/g;
let m;
while ((m = propRe.exec(line))) {
const s = m[2];
if (englishish.test(s) && !tailwindish.test(s) && !keySet.has(s) && !/\$\{/.test(s)) push(`[attr:${m[1]}] ${s}`, `${f.replace(root, ".")}:${i + 1}`);
}
continue;
}
// JSX 文本节点
const jsxRe = />\s*([^<>{}]*[A-Za-z][^<>{}]*?)\s*</g;
let m;
while ((m = jsxRe.exec(line))) {
const t = m[1].trim();
if (t.length < 3 || t.length > 90) continue;
if (!englishish.test(t) || tailwindish.test(t) || keySet.has(t)) continue;
if (/\$\{|=>|&&|\|\||\?\./.test(t)) continue;
if (/^[a-z][a-z0-9-]*$/.test(t)) continue;
if (/^(true|false|null|undefined|yes|no|ok|id|url|key|css|svg|png|jpg|jpeg|webp|mp4|mov|avi|mp3|wav|ogg|txt|json|xml|html|log|info|warn|error|debug|trace|none|auto|default)$/i.test(t)) continue;
push(`[jsx] ${t}`, `${f.replace(root, ".")}:${i + 1}`);
}
// toast/notify
const callRe = /\b(showToast|notify|enqueueToast|setStatus)\s*\(\s*["'`]([^"'`]{3,90})["'`]/g;
while ((m = callRe.exec(line))) {
const s = m[2];
if (englishish.test(s) && !tailwindish.test(s) && !keySet.has(s) && !/\$\{/.test(s)) push(`[toast] ${s}`, `${f.replace(root, ".")}:${i + 1}`);
}
}
}

const results = [...candidates.entries()].sort((a, b) => a[0].localeCompare(b[0]));
console.log("\n=== 确定缺失的英文 UI 文案:", results.length, "条 ===");
for (const [k, locs] of results) {
console.log(`${[...locs].join(" | ")}\t${k}`);
}
39 changes: 39 additions & 0 deletions scripts/check-zhcn.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 校验 ZH_CN 字典:完整性、重复 key、空值、残留英文
const fs = require("fs");
const p = fs.readFileSync("H:/AI/Clypra/src/i18n/I18nProvider.tsx", "utf8");
const zhCn = p.slice(p.indexOf("const ZH_CN"), p.indexOf("const EN_FROM_ZH_TW"));
const re = /"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
let m, cnt = 0, dup = 0, empty = 0;
const seen = new Set();
const entries = [];
while ((m = re.exec(zhCn))) {
cnt++;
if (seen.has(m[1])) dup++;
seen.add(m[1]);
entries.push([m[1], m[2]]);
if (!m[2].trim()) empty++;
}
console.log("ZH_CN 词条:", cnt, "| 重复key:", dup, "| 空值:", empty);

// 残留英文检测(值中出现的纯英文字母串,排除专有名词/技术词)
const whitelist = new Set([
"English","Whisper","FFmpeg","WebView","GPU","API","IndexedDB","JSON","PATH","Google","Tauri","React","Clypra",
"LUT","UltraKey","SRT","WebP","GitHub","Patreon","p95","WebGL","OpenCC","L/R","Cmd","Ctrl","Shift","Esc","Alt",
"CapCut","Pro","S","L","R","I","O","Q","W","F12","Hz","px","EV","iOS","Android","AI","Clip","Studio","MIDI","Ease",
"Base","Fill","GIF","MP4","MOV","WebM","MKV","MP3","WAV","PNG","H.264","H.265","AV1","HEVC","Patreon","K","M","T",
"SRT","LUT","3D","S-Curve","X","Y","Z","D","G","B","A","V","IDB","URL","CRT","HTTP","HTTPS","Web","WASM","Metal",
"DirectX","Vulkan","OpenGL","DVD","NLE","MS","kHz","Hz","ms","KHz","MB","GB","TB","px","fps","HDR","SDR","Lut",
]);
const englishWord = /\b[A-Za-z]{2,}\b/g;
const bad = [];
for (const [k, v] of entries) {
const words = v.match(englishWord) || [];
for (const w of words) {
if (!whitelist.has(w)) {
bad.push(`${JSON.stringify(k)} => ${JSON.stringify(v)} (词: ${w})`);
break;
}
}
}
console.log("疑似残留英文的值:", bad.length);
bad.slice(0, 30).forEach((b) => console.log(" ", b));
Loading