mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
Grid, offset-maker, liquidity-maker and maker-points wrote their trade log and Telegram alerts as Chinese literals, so LANG=en changed the menu but not a single runtime message. src/ now holds zero user-facing Chinese literals. Reuses rather than duplicates: the four engines' subscription boilerplate maps onto the existing log.subscribe.* / log.process.* keys, and the spot-maker wording shared by offset-maker and liquidity-maker became one log.spotMaker.* set instead of two. Exchange gateways were treated differently on purpose: aster's exception and console.error text is developer diagnostics, not UI, and its eight sibling gateways already used English — so those were translated to English rather than added to the table. Adds tests/i18n-coverage.test.ts to hold the line: it fails on any new CJK literal in src/, on a key missing either language, and on a duplicate key. Several existing tests asserted the Chinese literals, which is what let the gap persist; they now assert the resolved key and hold in either language. 214 new keys (300 -> 514). 275 pass; tsc and oxlint clean.
72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { t } from "../src/i18n";
|
|
|
|
const SRC = join(import.meta.dirname, "..", "src");
|
|
const I18N_FILE = join(SRC, "i18n", "index.ts");
|
|
|
|
function walk(dir: string, out: string[] = []): string[] {
|
|
for (const entry of readdirSync(dir)) {
|
|
const full = join(dir, entry);
|
|
if (statSync(full).isDirectory()) {
|
|
walk(full, out);
|
|
} else if (/\.tsx?$/.test(entry)) {
|
|
out.push(full);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** A string or template literal containing a CJK character. */
|
|
const CJK_IN_LITERAL = /["`][^"`\n]*[一-龥][^"`\n]*["`]/;
|
|
|
|
describe("i18n coverage", () => {
|
|
it("keeps user-facing text out of source files", () => {
|
|
// Chinese literals outside the translation table cannot be shown in English,
|
|
// which is how the order log, grid events and defense alerts stayed
|
|
// untranslatable for so long.
|
|
const offenders: string[] = [];
|
|
for (const file of walk(SRC)) {
|
|
if (file === I18N_FILE) continue;
|
|
if (file.endsWith(".test.ts") || file.endsWith(".test.tsx")) continue;
|
|
const lines = readFileSync(file, "utf8").split("\n");
|
|
lines.forEach((line, index) => {
|
|
if (line.trimStart().startsWith("//") || line.trimStart().startsWith("*")) return;
|
|
if (CJK_IN_LITERAL.test(line)) {
|
|
offenders.push(`${file.slice(SRC.length + 1)}:${index + 1} ${line.trim()}`);
|
|
}
|
|
});
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
it("gives every key both a zh and an en translation", () => {
|
|
const source = readFileSync(I18N_FILE, "utf8");
|
|
const table = source.slice(
|
|
source.indexOf("const translations"),
|
|
source.indexOf("const formatTemplate")
|
|
);
|
|
const keys = [...table.matchAll(/^ {2}"([\w.]+)":/gm)].map((m) => m[1]!);
|
|
expect(keys.length).toBeGreaterThan(400);
|
|
|
|
const duplicates = keys.filter((key, index) => keys.indexOf(key) !== index);
|
|
expect(duplicates).toEqual([]);
|
|
|
|
for (const key of keys) {
|
|
expect(t(key, {}, "zh"), `${key} missing zh`).not.toBe(key);
|
|
expect(t(key, {}, "en"), `${key} missing en`).not.toBe(key);
|
|
}
|
|
});
|
|
|
|
it("substitutes placeholders in both languages", () => {
|
|
expect(t("log.order.closePlaced", { side: "BUY" }, "zh")).toContain("BUY");
|
|
expect(t("log.order.closePlaced", { side: "BUY" }, "en")).toContain("BUY");
|
|
expect(t("log.order.closePlaced", { side: "BUY" }, "en")).not.toContain("{side}");
|
|
});
|
|
|
|
it("leaves an unknown placeholder visible rather than printing undefined", () => {
|
|
expect(t("log.order.closePlaced", {}, "en")).toContain("{side}");
|
|
});
|
|
});
|