mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat(groups): configure message language per group instead of per route
Route.lang is removed; Group.lang now drives the message language for every route in the group (default en, custom via KV i18n:<lang>). dispatch.ts loads translations per group and falls back to en for groups without a lang. Admin UI: GroupEditor gains the language field, RouteEditor/RouteCard drop theirs; docs and config example updated.
This commit is contained in:
parent
c73b642504
commit
7e45b0a09c
12 changed files with 114 additions and 40 deletions
|
|
@ -32,6 +32,27 @@
|
||||||
<div class="hint">{{ t("groupEditor.idHint") }}</div>
|
<div class="hint">{{ t("groupEditor.idHint") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row2">
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ t("groupEditor.language") }}</label>
|
||||||
|
<input
|
||||||
|
v-model="form.lang"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('groupEditor.langPlaceholder')"
|
||||||
|
/>
|
||||||
|
<div class="hint">{{ t("groupEditor.langHint") }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label
|
||||||
|
>{{ t("groupEditor.emoji") }}
|
||||||
|
<span class="lbl-note">{{ t("groupEditor.emojiNote") }}</span></label
|
||||||
|
>
|
||||||
|
<label class="inline">
|
||||||
|
<input v-model="form.emoji" type="checkbox" />
|
||||||
|
<span>{{ t("groupEditor.emojiLabel") }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label
|
<label
|
||||||
>{{ t("groupEditor.membersNote") }}
|
>{{ t("groupEditor.membersNote") }}
|
||||||
|
|
@ -85,10 +106,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="hint">{{ t("groupEditor.providersHint") }}</div>
|
<div class="hint">{{ t("groupEditor.providersHint") }}</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="inline">
|
|
||||||
<input v-model="form.emoji" type="checkbox" />
|
|
||||||
<span>{{ t("groupEditor.emoji") }}</span>
|
|
||||||
</label>
|
|
||||||
<div class="err">{{ formError }}</div>
|
<div class="err">{{ formError }}</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="editor-foot">
|
<div class="editor-foot">
|
||||||
|
|
@ -126,6 +143,7 @@ const form = reactive({
|
||||||
owners: "",
|
owners: "",
|
||||||
providers: [] as ("github" | "gitea")[],
|
providers: [] as ("github" | "gitea")[],
|
||||||
emoji: true,
|
emoji: true,
|
||||||
|
lang: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
function splitList(text: string): string[] {
|
function splitList(text: string): string[] {
|
||||||
|
|
@ -154,6 +172,7 @@ watch(
|
||||||
(p): p is "github" | "gitea" => p === "github" || p === "gitea",
|
(p): p is "github" | "gitea" => p === "github" || p === "gitea",
|
||||||
);
|
);
|
||||||
form.emoji = g?.emoji ?? true;
|
form.emoji = g?.emoji ?? true;
|
||||||
|
form.lang = g?.lang ?? "";
|
||||||
formError.value = "";
|
formError.value = "";
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -188,6 +207,7 @@ function save(): void {
|
||||||
owners: props.superAdmin ? (owners.length ? owners : undefined) : props.group?.owners,
|
owners: props.superAdmin ? (owners.length ? owners : undefined) : props.group?.owners,
|
||||||
providers: form.providers.length ? form.providers : undefined,
|
providers: form.providers.length ? form.providers : undefined,
|
||||||
emoji: form.emoji,
|
emoji: form.emoji,
|
||||||
|
lang: form.lang.trim() || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
<span v-if="readonly" class="dot" :class="route.enabled ? 'ok' : 'bad'"></span>
|
<span v-if="readonly" class="dot" :class="route.enabled ? 'ok' : 'bad'"></span>
|
||||||
<span class="route-name">{{ route.name || t("route.untitled") }}</span>
|
<span class="route-name">{{ route.name || t("route.untitled") }}</span>
|
||||||
<span class="route-id">{{ route.id }}</span>
|
<span class="route-id">{{ route.id }}</span>
|
||||||
<span v-if="route.lang" class="badge lang">{{ route.lang }}</span>
|
|
||||||
<span
|
<span
|
||||||
v-for="(tg, i) in route.targets"
|
v-for="(tg, i) in route.targets"
|
||||||
:key="i"
|
:key="i"
|
||||||
|
|
|
||||||
|
|
@ -37,22 +37,11 @@
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row2">
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t("routeEditor.id") }}</label>
|
<label>{{ t("routeEditor.id") }}</label>
|
||||||
<input v-model="form.id" type="text" placeholder="my-route" required />
|
<input v-model="form.id" type="text" placeholder="my-route" required />
|
||||||
<div class="hint">{{ t("routeEditor.idHint") }}</div>
|
<div class="hint">{{ t("routeEditor.idHint") }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label>{{ t("routeEditor.language") }}</label>
|
|
||||||
<input
|
|
||||||
v-model="form.lang"
|
|
||||||
type="text"
|
|
||||||
:placeholder="t('routeEditor.langPlaceholder')"
|
|
||||||
/>
|
|
||||||
<div class="hint">{{ t("routeEditor.langHint") }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field inline">
|
<div class="field inline">
|
||||||
<input v-model="form.enabled" type="checkbox" />
|
<input v-model="form.enabled" type="checkbox" />
|
||||||
<span>{{ t("routeEditor.enabled") }}</span>
|
<span>{{ t("routeEditor.enabled") }}</span>
|
||||||
|
|
@ -211,7 +200,6 @@ function blankTarget(): TargetForm {
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
lang: "",
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fallback: false,
|
fallback: false,
|
||||||
stop: false,
|
stop: false,
|
||||||
|
|
@ -263,7 +251,6 @@ watch(
|
||||||
const r = props.route;
|
const r = props.route;
|
||||||
form.id = r?.id ?? "";
|
form.id = r?.id ?? "";
|
||||||
form.name = r?.name ?? "";
|
form.name = r?.name ?? "";
|
||||||
form.lang = r?.lang ?? "";
|
|
||||||
form.enabled = r?.enabled ?? true;
|
form.enabled = r?.enabled ?? true;
|
||||||
form.fallback = r?.fallback ?? false;
|
form.fallback = r?.fallback ?? false;
|
||||||
form.stop = r?.stop ?? false;
|
form.stop = r?.stop ?? false;
|
||||||
|
|
@ -340,7 +327,6 @@ function collect(): Route | null {
|
||||||
enabled: form.enabled,
|
enabled: form.enabled,
|
||||||
fallback: form.fallback || undefined,
|
fallback: form.fallback || undefined,
|
||||||
stop: form.stop || undefined,
|
stop: form.stop || undefined,
|
||||||
lang: form.lang.trim() || undefined,
|
|
||||||
discordRoleIds: discordRoles.length ? discordRoles : undefined,
|
discordRoleIds: discordRoles.length ? discordRoles : undefined,
|
||||||
filters,
|
filters,
|
||||||
targets,
|
targets,
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,6 @@ const en: Dict = {
|
||||||
"routeEditor.namePlaceholder": "My Route",
|
"routeEditor.namePlaceholder": "My Route",
|
||||||
"routeEditor.id": "ID",
|
"routeEditor.id": "ID",
|
||||||
"routeEditor.idHint": "Unique, use a-z / 0-9 / dashes",
|
"routeEditor.idHint": "Unique, use a-z / 0-9 / dashes",
|
||||||
"routeEditor.language": "Language",
|
|
||||||
"routeEditor.langPlaceholder": "en",
|
|
||||||
"routeEditor.langHint": "en or zh; custom via KV i18n:<lang>",
|
|
||||||
"routeEditor.enabled": "Route enabled",
|
"routeEditor.enabled": "Route enabled",
|
||||||
"routeEditor.fallback": "Fallback route",
|
"routeEditor.fallback": "Fallback route",
|
||||||
"routeEditor.fallbackHint": "Only fires when no other route matched; filters still apply",
|
"routeEditor.fallbackHint": "Only fires when no other route matched; filters still apply",
|
||||||
|
|
@ -148,6 +145,11 @@ const en: Dict = {
|
||||||
"groupEditor.id": "ID",
|
"groupEditor.id": "ID",
|
||||||
"groupEditor.idPlaceholder": "my-team",
|
"groupEditor.idPlaceholder": "my-team",
|
||||||
"groupEditor.idHint": "Unique, use a-z / 0-9 / dashes",
|
"groupEditor.idHint": "Unique, use a-z / 0-9 / dashes",
|
||||||
|
"groupEditor.language": "Message language",
|
||||||
|
"groupEditor.langPlaceholder": "en",
|
||||||
|
"groupEditor.langHint": "en or zh; custom via KV i18n:<lang>",
|
||||||
|
"groupEditor.emojiNote": "(default on)",
|
||||||
|
"groupEditor.emojiLabel": "Show emojis in messages",
|
||||||
"groupEditor.admins": "Group admins",
|
"groupEditor.admins": "Group admins",
|
||||||
"groupEditor.adminsNote": "(GitHub login or user id, comma-separated)",
|
"groupEditor.adminsNote": "(GitHub login or user id, comma-separated)",
|
||||||
"groupEditor.adminsPlaceholder": "octocat, 12345",
|
"groupEditor.adminsPlaceholder": "octocat, 12345",
|
||||||
|
|
@ -314,9 +316,6 @@ const zh: Dict = {
|
||||||
"routeEditor.namePlaceholder": "我的路由",
|
"routeEditor.namePlaceholder": "我的路由",
|
||||||
"routeEditor.id": "ID",
|
"routeEditor.id": "ID",
|
||||||
"routeEditor.idHint": "唯一,使用 a-z / 0-9 / 短横线",
|
"routeEditor.idHint": "唯一,使用 a-z / 0-9 / 短横线",
|
||||||
"routeEditor.language": "语言",
|
|
||||||
"routeEditor.langPlaceholder": "zh",
|
|
||||||
"routeEditor.langHint": "en 或 zh;可通过 KV i18n:<lang> 自定义",
|
|
||||||
"routeEditor.enabled": "启用路由",
|
"routeEditor.enabled": "启用路由",
|
||||||
"routeEditor.fallback": "兜底路由",
|
"routeEditor.fallback": "兜底路由",
|
||||||
"routeEditor.fallbackHint": "仅当没有其它路由匹配该事件时才发送,其过滤器仍会生效",
|
"routeEditor.fallbackHint": "仅当没有其它路由匹配该事件时才发送,其过滤器仍会生效",
|
||||||
|
|
@ -362,6 +361,11 @@ const zh: Dict = {
|
||||||
"groupEditor.id": "ID",
|
"groupEditor.id": "ID",
|
||||||
"groupEditor.idPlaceholder": "my-team",
|
"groupEditor.idPlaceholder": "my-team",
|
||||||
"groupEditor.idHint": "唯一,使用 a-z / 0-9 / 短横线",
|
"groupEditor.idHint": "唯一,使用 a-z / 0-9 / 短横线",
|
||||||
|
"groupEditor.language": "消息语言",
|
||||||
|
"groupEditor.langPlaceholder": "zh",
|
||||||
|
"groupEditor.langHint": "en 或 zh;可通过 KV i18n:<lang> 自定义",
|
||||||
|
"groupEditor.emojiNote": "(默认开启)",
|
||||||
|
"groupEditor.emojiLabel": "消息中显示表情符号",
|
||||||
"groupEditor.admins": "分组管理员",
|
"groupEditor.admins": "分组管理员",
|
||||||
"groupEditor.adminsNote": "(GitHub 登录名或用户 ID,逗号分隔)",
|
"groupEditor.adminsNote": "(GitHub 登录名或用户 ID,逗号分隔)",
|
||||||
"groupEditor.adminsPlaceholder": "octocat, 12345",
|
"groupEditor.adminsPlaceholder": "octocat, 12345",
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ export interface Route {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
targets: RouteTarget[];
|
targets: RouteTarget[];
|
||||||
lang?: string;
|
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
fallback?: boolean;
|
fallback?: boolean;
|
||||||
stop?: boolean;
|
stop?: boolean;
|
||||||
|
|
@ -40,6 +39,7 @@ export interface Group {
|
||||||
owners?: string[];
|
owners?: string[];
|
||||||
providers?: ("github" | "gitea" | "gitlab")[];
|
providers?: ("github" | "gitea" | "gitlab")[];
|
||||||
emoji?: boolean;
|
emoji?: boolean;
|
||||||
|
lang?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupInvite {
|
export interface GroupInvite {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ groups:
|
||||||
# owners: ["myorg"] # restrict events to this org/user (optional)
|
# owners: ["myorg"] # restrict events to this org/user (optional)
|
||||||
# providers: ["github", "gitea"] # restrict to source platforms (optional; empty = all)
|
# providers: ["github", "gitea"] # restrict to source platforms (optional; empty = all)
|
||||||
# emoji: true # include emoji in messages (default true)
|
# emoji: true # include emoji in messages (default true)
|
||||||
|
# lang: "en" # message language for this group's routes (en/zh, default en)
|
||||||
|
|
||||||
routes:
|
routes:
|
||||||
- id: all-push
|
- id: all-push
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,6 @@ Other route fields:
|
||||||
| `groupId` | string | Yes | Id of the [group](#groups) this route belongs to |
|
| `groupId` | string | Yes | Id of the [group](#groups) this route belongs to |
|
||||||
| `fallback` | boolean | No | When `true`, fires only if no non-fallback route matched the event; its own filters are ignored |
|
| `fallback` | boolean | No | When `true`, fires only if no non-fallback route matched the event; its own filters are ignored |
|
||||||
| `stop` | boolean | No | When `true` and this route matches, no further routes are evaluated for this event |
|
| `stop` | boolean | No | When `true` and this route matches, no further routes are evaluated for this event |
|
||||||
| `lang` | string | No | Message language override for this route (e.g. `en`, `zh`); defaults to the global setting |
|
|
||||||
| `discordRoleIds` | string[] | No | Discord role ids to ping when this route fires; applied to Discord targets only |
|
| `discordRoleIds` | string[] | No | Discord role ids to ping when this route fires; applied to Discord targets only |
|
||||||
|
|
||||||
### Custom Route Example
|
### Custom Route Example
|
||||||
|
|
@ -195,6 +194,7 @@ Routes belong to groups. Groups scope admin access and can restrict which events
|
||||||
| `owners` | string[] | No | Org/user logins whose events are accepted into this group; empty = all |
|
| `owners` | string[] | No | Org/user logins whose events are accepted into this group; empty = all |
|
||||||
| `providers` | string[] | No | Source platforms allowed into this group (`github`, `gitea`); empty = all |
|
| `providers` | string[] | No | Source platforms allowed into this group (`github`, `gitea`); empty = all |
|
||||||
| `emoji` | boolean | No | Whether to include emoji in this group's messages (default `true`) |
|
| `emoji` | boolean | No | Whether to include emoji in this group's messages (default `true`) |
|
||||||
|
| `lang` | string | No | Message language for every route in this group (e.g. `en`, `zh`; custom via KV `i18n:<lang>`) — defaults to `en` |
|
||||||
|
|
||||||
### Roles
|
### Roles
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,6 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
|
||||||
| `groupId` | string | 是 | 该路由所属[分组](#分组)的 id |
|
| `groupId` | string | 是 | 该路由所属[分组](#分组)的 id |
|
||||||
| `fallback` | boolean | 否 | 为 `true` 时,仅当没有其它路由匹配该事件时才发送,其自身过滤器会被忽略 |
|
| `fallback` | boolean | 否 | 为 `true` 时,仅当没有其它路由匹配该事件时才发送,其自身过滤器会被忽略 |
|
||||||
| `stop` | boolean | 否 | 为 `true` 且该路由匹配时,停止评估后续路由 |
|
| `stop` | boolean | 否 | 为 `true` 且该路由匹配时,停止评估后续路由 |
|
||||||
| `lang` | string | 否 | 该路由的消息语言覆盖(如 `en`、`zh`),默认跟随全局设置 |
|
|
||||||
| `discordRoleIds` | string[] | 否 | 该路由触发时要在 Discord 目标中 @提醒的身份组 id |
|
| `discordRoleIds` | string[] | 否 | 该路由触发时要在 Discord 目标中 @提醒的身份组 id |
|
||||||
|
|
||||||
### 自定义路由示例
|
### 自定义路由示例
|
||||||
|
|
@ -195,6 +194,7 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
|
||||||
| `owners` | string[] | 否 | 允许事件进入该分组的组织/用户登录名;为空表示不限制 |
|
| `owners` | string[] | 否 | 允许事件进入该分组的组织/用户登录名;为空表示不限制 |
|
||||||
| `providers` | string[] | 否 | 允许进入该分组的来源平台(`github`、`gitea`);为空表示全部 |
|
| `providers` | string[] | 否 | 允许进入该分组的来源平台(`github`、`gitea`);为空表示全部 |
|
||||||
| `emoji` | boolean | 否 | 是否在该分组消息中显示 emoji(默认 `true`) |
|
| `emoji` | boolean | 否 | 是否在该分组消息中显示 emoji(默认 `true`) |
|
||||||
|
| `lang` | string | 否 | 该分组所有路由的消息语言(如 `en`、`zh`;可通过 KV `i18n:<lang>` 自定义)——默认 `en` |
|
||||||
|
|
||||||
### 角色
|
### 角色
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -259,4 +259,62 @@ describe("dispatchEvent fallback routing", () => {
|
||||||
expect(sent.filter((u) => u.includes("/222/"))).toHaveLength(1);
|
expect(sent.filter((u) => u.includes("/222/"))).toHaveLength(1);
|
||||||
expect(sent.filter((u) => u.includes("/111/"))).toHaveLength(0);
|
expect(sent.filter((u) => u.includes("/111/"))).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the group's message language (not the route's)", async () => {
|
||||||
|
const bodies: string[] = [];
|
||||||
|
mockFetch((url, init) => {
|
||||||
|
bodies.push(String(init?.body ?? ""));
|
||||||
|
return new Response("{}", { status: 200 });
|
||||||
|
});
|
||||||
|
const kv = createMockKV();
|
||||||
|
await kv.put(
|
||||||
|
"config:groups",
|
||||||
|
JSON.stringify([
|
||||||
|
{ id: "zh-group", name: "中文组", adminIds: [], lang: "zh" },
|
||||||
|
{ id: "en-group", name: "English", adminIds: [] },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const env = createEnv({ KV: kv, DB: createMockDB() });
|
||||||
|
const routes: Route[] = [
|
||||||
|
{
|
||||||
|
id: "zh-push",
|
||||||
|
name: "ZH",
|
||||||
|
enabled: true,
|
||||||
|
groupId: "zh-group",
|
||||||
|
filters: [{ type: "event", match: "push" }],
|
||||||
|
targets: [{ channelId: "111" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "en-push",
|
||||||
|
name: "EN",
|
||||||
|
enabled: true,
|
||||||
|
groupId: "en-group",
|
||||||
|
filters: [{ type: "event", match: "push" }],
|
||||||
|
targets: [{ channelId: "222" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await dispatchEvent(
|
||||||
|
{ ...baseConfig, routes },
|
||||||
|
{
|
||||||
|
event: "push",
|
||||||
|
payload: {
|
||||||
|
repository: { full_name: "owner/repo" },
|
||||||
|
commits: [{ id: "abc", message: "fix", author: { name: "a" } }],
|
||||||
|
ref: "refs/heads/main",
|
||||||
|
compare: "https://example.com/compare",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(bodies).toHaveLength(2);
|
||||||
|
const titles = bodies.map((b) => {
|
||||||
|
const parsed = JSON.parse(b) as { embeds?: Array<{ title?: string }> };
|
||||||
|
return parsed.embeds?.[0]?.title ?? "";
|
||||||
|
});
|
||||||
|
expect(titles.sort()).toEqual(
|
||||||
|
["owner/repo: 推送了 1 个提交", "owner/repo: Pushed 1 commit"].sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ import { getDriver } from "../drivers";
|
||||||
import type { SendResult } from "../drivers/types";
|
import type { SendResult } from "../drivers/types";
|
||||||
|
|
||||||
export async function dispatchEvent(config: Config, event: WebhookEvent, env: Env): Promise<void> {
|
export async function dispatchEvent(config: Config, event: WebhookEvent, env: Env): Promise<void> {
|
||||||
const langs = [...new Set(config.routes.map((r) => r.lang ?? "en"))];
|
const groups = await loadGroups(env.KV);
|
||||||
|
const groupById = new Map(groups.map((g) => [g.id, g]));
|
||||||
|
|
||||||
|
// Message language is configured per group (Group.lang), not per route.
|
||||||
|
const langs = [...new Set(groups.map((g) => g.lang ?? "en"))];
|
||||||
const trMap = new Map<string, Translations>();
|
const trMap = new Map<string, Translations>();
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
langs.map(async (lang) => {
|
langs.map(async (lang) => {
|
||||||
|
|
@ -17,8 +21,6 @@ export async function dispatchEvent(config: Config, event: WebhookEvent, env: En
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const groups = await loadGroups(env.KV);
|
|
||||||
const groupById = new Map(groups.map((g) => [g.id, g]));
|
|
||||||
const owners = eventOwners(event);
|
const owners = eventOwners(event);
|
||||||
|
|
||||||
const accepted = (route: Route): boolean => {
|
const accepted = (route: Route): boolean => {
|
||||||
|
|
@ -53,8 +55,8 @@ export async function dispatchEvent(config: Config, event: WebhookEvent, env: En
|
||||||
const targets = route.targets && route.targets.length > 0 ? route.targets : [];
|
const targets = route.targets && route.targets.length > 0 ? route.targets : [];
|
||||||
if (targets.length === 0) return;
|
if (targets.length === 0) return;
|
||||||
|
|
||||||
const tr = trMap.get(route.lang ?? "en")!;
|
|
||||||
const group = route.groupId ? groupById.get(route.groupId) : undefined;
|
const group = route.groupId ? groupById.get(route.groupId) : undefined;
|
||||||
|
const tr = trMap.get(group?.lang ?? "en")!;
|
||||||
const showEmoji = group?.emoji !== false;
|
const showEmoji = group?.emoji !== false;
|
||||||
const message = formatEvent(route, event, tr, showEmoji);
|
const message = formatEvent(route, event, tr, showEmoji);
|
||||||
if (route.discordRoleIds?.length) {
|
if (route.discordRoleIds?.length) {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ export interface Route {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
targets: RouteTarget[];
|
targets: RouteTarget[];
|
||||||
lang?: string;
|
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
/**
|
/**
|
||||||
* Fallback route: only fires when no other (non-fallback) route matched the
|
* Fallback route: only fires when no other (non-fallback) route matched the
|
||||||
|
|
@ -120,6 +119,11 @@ export interface Group {
|
||||||
* Defaults to true when omitted.
|
* Defaults to true when omitted.
|
||||||
*/
|
*/
|
||||||
emoji?: boolean;
|
emoji?: boolean;
|
||||||
|
/**
|
||||||
|
* Message language for every route in this group (e.g. "en", "zh"; custom
|
||||||
|
* via KV i18n:<lang>). Defaults to "en" when omitted.
|
||||||
|
*/
|
||||||
|
lang?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,6 @@ function validateRoutes(
|
||||||
}
|
}
|
||||||
if (typeof r.enabled !== "boolean")
|
if (typeof r.enabled !== "boolean")
|
||||||
return { ok: false, error: `route "${r.id}".enabled must be boolean` };
|
return { ok: false, error: `route "${r.id}".enabled must be boolean` };
|
||||||
if (r.lang !== undefined && typeof r.lang !== "string") {
|
|
||||||
return { ok: false, error: `route "${r.id}".lang must be a string` };
|
|
||||||
}
|
|
||||||
if (r.fallback !== undefined && typeof r.fallback !== "boolean") {
|
if (r.fallback !== undefined && typeof r.fallback !== "boolean") {
|
||||||
return { ok: false, error: `route "${r.id}".fallback must be a boolean` };
|
return { ok: false, error: `route "${r.id}".fallback must be a boolean` };
|
||||||
}
|
}
|
||||||
|
|
@ -283,6 +280,9 @@ function validateGroups(
|
||||||
if (g.emoji !== undefined && typeof g.emoji !== "boolean") {
|
if (g.emoji !== undefined && typeof g.emoji !== "boolean") {
|
||||||
return { ok: false, error: `group "${g.id}".emoji must be a boolean` };
|
return { ok: false, error: `group "${g.id}".emoji must be a boolean` };
|
||||||
}
|
}
|
||||||
|
if (g.lang !== undefined && typeof g.lang !== "string") {
|
||||||
|
return { ok: false, error: `group "${g.id}".lang must be a string` };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { ok: true, groups: groups as Group[] };
|
return { ok: true, groups: groups as Group[] };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue