chore: add CI, lint config, login rate limit and split large modules

- Rate-limit failed logins (8 per 15 min per IP or username, 429 + Retry-After)
- Add ruff/mypy config and GitHub Actions workflow (ruff, pytest, tsc, tests, build)
- Apply ruff autofixes (import order, pyupgrade)
- Extract rpy_helpers.py, engine-util.ts, TranslationParts.tsx and
  translation-types.ts from oversized modules
- Add dependency-free React UI tests
- Refresh README and ARCHITECTURE (Naninovel/Tomie, status and plans)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-25 11:23:22 +03:00
1 parent 57a4edcbfd
commit bbbcc6a5d8
42 files changed
+678 -391

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
// Bundle React together with the code under test so that both share one React instance.
async function load(entry) {
const result = await build({
stdin: { contents: entry, resolveDir: fileURLToPath(new URL("../src/", import.meta.url)), loader: "tsx" },
bundle: true, write: false, format: "esm", platform: "node", jsx: "automatic", logLevel: "silent",
banner: { js: "import { createRequire } from \"node:module\"; const require = createRequire(process.cwd() + \"/package.json\");" },
});
const code = Buffer.from(result.outputFiles[0].text).toString("base64");
return import(`data:text/javascript;base64,${code}`);
}
const { renderToStaticMarkup, h, ProgressBar, highlight } = await load(`
export { renderToStaticMarkup } from "react-dom/server";
export { createElement as h } from "react";
export { ProgressBar, highlight } from "./pages/TranslationParts";
`);
const stats = {
lines: 10, translated: 5, progress: 0.5, strings: 4, stringsTranslated: 2,
states: { untranslated: 5, outdated: 1, translated: 3, fuzzy: 0, machine: 0, approved: 1 },
};
test("ProgressBar renders counts and only non-empty segments", () => {
const html = renderToStaticMarkup(h(ProgressBar, { stats }));
assert.match(html, /5 из 10 реплик \(50%\)/);
assert.match(html, /устарело 1/);
assert.match(html, /seg-approved/);
assert.doesNotMatch(html, /seg-warn/);
});
test("highlight wraps glossary terms case-insensitively and escapes regex characters", () => {
const terms = [{ src: "Eileen", t: "Эйлин", note: "" }, { src: "a+b", t: "x", note: "" }];
const html = renderToStaticMarkup(h("p", null, highlight("eileen met a+b", terms)));
assert.match(html, /<mark title="Эйлин">eileen<\/mark>/);
assert.match(html, /<mark title="x">a\+b<\/mark>/);
assert.equal(highlight("plain", []), "plain");
});