mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
i18n: route the remaining 300 hardcoded strings through the table
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.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { t } from "../src/i18n";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||
@@ -103,7 +104,11 @@ describe("MakerPointsEngine Binance depth health defense", () => {
|
||||
expect((engine as any).defenseMode).toBe(true);
|
||||
|
||||
const logs = ((engine as any).tradeLog.all() as Array<{ detail: string }>).map((entry) => entry.detail);
|
||||
expect(logs.some((detail) => detail.includes("Binance簿记异常(orderbook_not_ready)"))).toBe(true);
|
||||
expect(
|
||||
logs.some((detail) =>
|
||||
detail.includes(t("defense.reason.binanceBook", { reason: "orderbook_not_ready" }))
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
engine.stop();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
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}");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user