* refactor(channels): own setup and instance contracts * refactor(channels): isolate management contracts * refactor(channels): normalize activation contracts * fix(channels): enforce management contracts * refactor(channels): finish setup ownership migration * fix(channels): harden management contracts * fix(channels): enforce lazy loading and runtime ownership * fix(feishu): make multi-instance startup idempotent * fix(webui): render channel setup contracts cleanly * fix(feishu): stop websocket clients cleanly * fix(channels): enforce persistence and activation gates * fix(channels): preserve global feature action scope * fix(channels): apply defaults for single plugins * fix(channels): enforce management contract boundaries * refactor(feishu): remove identity helper indirection * fix(channels): preserve management setup contracts * refactor(channels): generalize instance settings UI * refactor(channels): package channel plugins with web UI metadata * refactor(channels): make built-ins self-contained packages * test(channels): colocate tests with channel packages * fix(dingtalk): use official brand icon * feat(channels): colocate webui translations * docs(channels): clarify plugin ownership * test(exec): remove output wait race * refactor(channels): unify plugin descriptors * fix(channels): enforce descriptor-owned contracts * refactor(channels): finish package-owned plugin setup * refactor(channels): use repository-owned packages only * fix(channels): self-describe dependencies and runtime state * fix(channels): warn about legacy entry points
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { channelFieldMessageKey } from "@/channel-plugins/i18n";
|
|
import { registeredChannelLocales } from "@/channel-plugins/locale-registry";
|
|
import { registeredChannelUiContributions } from "@/channel-plugins/registry";
|
|
import { supportedLocales } from "@/i18n/config";
|
|
|
|
const expectedChannels = [
|
|
"dingtalk",
|
|
"discord",
|
|
"email",
|
|
"feishu",
|
|
"matrix",
|
|
"mattermost",
|
|
"msteams",
|
|
"napcat",
|
|
"qq",
|
|
"signal",
|
|
"slack",
|
|
"telegram",
|
|
"websocket",
|
|
"wecom",
|
|
"weixin",
|
|
"whatsapp",
|
|
];
|
|
|
|
function flatten(value: unknown, prefix = ""): Map<string, string> {
|
|
const entries = new Map<string, string>();
|
|
if (typeof value === "string") {
|
|
entries.set(prefix, value);
|
|
return entries;
|
|
}
|
|
if (!value || typeof value !== "object") return entries;
|
|
|
|
for (const [key, child] of Object.entries(value)) {
|
|
if (!prefix && key === "displayName") continue;
|
|
const childPrefix = prefix ? `${prefix}.${key}` : key;
|
|
for (const [childKey, text] of flatten(child, childPrefix)) {
|
|
entries.set(childKey, text);
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function interpolationKeys(value: string): string[] {
|
|
return [...value.matchAll(/{{\s*([\w.-]+)\s*}}/g)]
|
|
.map((match) => match[1])
|
|
.sort();
|
|
}
|
|
|
|
describe("channel locale registry", () => {
|
|
it("loads every supported locale from every built-in channel package", () => {
|
|
const registrations = registeredChannelLocales();
|
|
expect([...registrations.keys()].sort()).toEqual(expectedChannels);
|
|
|
|
for (const [channel, locales] of registrations) {
|
|
expect([...locales.keys()].sort()).toEqual(
|
|
supportedLocales.map(({ code }) => code).sort(),
|
|
);
|
|
|
|
const english = flatten(locales.get("en"));
|
|
for (const [locale, messages] of locales) {
|
|
const translated = flatten(messages);
|
|
expect([...translated.keys()].sort(), `${channel}/${locale} message keys`).toEqual(
|
|
[...english.keys()].sort(),
|
|
);
|
|
for (const [key, source] of english) {
|
|
expect(
|
|
interpolationKeys(translated.get(key) ?? ""),
|
|
`${channel}/${locale}:${key} interpolation keys`,
|
|
).toEqual(interpolationKeys(source));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it("keeps structural UI definitions aligned with English locale keys", () => {
|
|
const locales = registeredChannelLocales();
|
|
for (const { channel, contribution } of registeredChannelUiContributions()) {
|
|
const messages = locales.get(channel)?.get("en");
|
|
expect(messages, `${channel} English messages`).toBeDefined();
|
|
|
|
const setup = contribution.presentation.setup;
|
|
for (const field of [...(setup?.fields ?? []), ...(setup?.manualFields ?? [])]) {
|
|
const messageKey = channelFieldMessageKey(channel, field.key);
|
|
expect(messages?.setup.fields?.[messageKey], `${channel} field ${messageKey}`).toBeDefined();
|
|
}
|
|
for (const action of setup?.actions ?? []) {
|
|
expect(messages?.setup.actions?.[action.id], `${channel} action ${action.id}`).toBeTypeOf("string");
|
|
}
|
|
for (const preset of setup?.presets ?? []) {
|
|
expect(messages?.setup.presets?.[preset.id], `${channel} preset ${preset.id}`).toBeTypeOf("string");
|
|
}
|
|
}
|
|
});
|
|
|
|
it("keeps i18n initialization independent from channel React modules", () => {
|
|
const localeRegistry = readFileSync(
|
|
resolve(process.cwd(), "src/channel-plugins/locale-registry.ts"),
|
|
"utf8",
|
|
);
|
|
const i18nEntry = readFileSync(resolve(process.cwd(), "src/i18n/index.ts"), "utf8");
|
|
|
|
expect(localeRegistry).toContain("webui/locales/*.json");
|
|
expect(localeRegistry).not.toMatch(/channel-plugins\/registry|\.tsx|\breact\b/i);
|
|
expect(i18nEntry).toContain("channel-plugins/locale-registry");
|
|
expect(i18nEntry).not.toContain("channel-plugins/registry");
|
|
});
|
|
});
|