Auto Router, Discord Rules Page, & Vi18n lib (#52)

feat: Auto Router, Discord Rules Page, & Vi18n lib (#52)
This commit is contained in:
Benjamin Singleton 2026-04-04 03:03:41 -05:00 committed by GitHub
parent 262b8c578f
commit 8c59485898
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 3479 additions and 782 deletions

View file

@ -0,0 +1,636 @@
import type { Result, Value } from "@/utils/types";
import {
computeAllVariants,
selectionChainToString,
type PatternVariant,
type UncompiledLocale,
} from "./setup";
import type { FluentBundle, FluentVariable, Message } from "@fluent/bundle";
import { parseMarkdown, type Markdown } from "./markdown";
import {
configMarkdownSymbol,
configMessageTypeSymbol,
configStringSymbol,
type ConfigMarkdown,
type ConfigString,
type InferLocaleFromConfig,
type LocaleConfig,
} from "./config";
import type { Pattern } from "@fluent/bundle/esm/ast";
type GenericLocale = { [id: string]: GenericLocale | GenericMessageFn };
type GenericMessageFn = GenericStringMessageFn | GenericMarkdownMessageFn;
type GenericStringMessageFn = (
placeableArgs?: GenericMessageFnPlaceableArgs,
) => string;
type GenericMarkdownMessageFn = (
placeableArgs?: GenericMessageFnPlaceableArgs,
) => Markdown;
type GenericMessageFnPlaceableArgs = Record<string, PlaceableValue>;
type PlaceableValue = string | number;
export interface CompileLocaleCtx<Config extends LocaleConfig> {
bundle: FluentBundle;
uncompiled: UncompiledLocale;
config: Config;
fallback: InferLocaleFromConfig<Config> | undefined;
messageIdChain: readonly string[];
}
export interface CompileLocaleRes<Locale> {
locale: Locale;
errors: readonly string[];
}
export function compileLocale<Config extends LocaleConfig>(
ctx: CompileLocaleCtx<Config>,
): CompileLocaleRes<InferLocaleFromConfig<Config>> {
const {
bundle,
uncompiled,
config,
fallback,
messageIdChain: localeMessageIdChain = [],
} = ctx;
const errors: string[] = [];
const uncompiledKeys = new Set(Object.keys(uncompiled ?? {}));
const configKeys = new Set(Object.keys(config));
const excessKeys = uncompiledKeys.difference(configKeys);
if (excessKeys.size > 0) {
errors.push(`Excess keys in record: ${[...excessKeys].join(", ")}`);
}
const locale: GenericLocale = {};
for (const [messageId, configValue] of Object.entries(config)) {
const uncompiledValue = uncompiled?.[messageId];
const fallbackValue = fallback?.[messageId];
const valueMessageIdChain = [
...localeMessageIdChain,
messageId,
] as const;
const compiledValue = ((): Value<GenericLocale> => {
if (configMessageTypeSymbol in configValue) {
const compiledMessage = (() => {
if (uncompiledValue?.type !== "message") {
errors.push(
`Expected message for key \`${messageId}\`, found: ${typeof uncompiledValue}`,
);
return undefined;
}
const uncompiledMessage = uncompiledValue.message;
const compiledMessageRes = compileMessage({
bundle,
messageIdChain: valueMessageIdChain,
configValue,
uncompiledMessage,
});
if (compiledMessageRes.type === "err") {
errors.push(compiledMessageRes.err);
return undefined;
}
const compiledMessage = compiledMessageRes.ok;
return compiledMessage;
})();
if (compiledMessage !== undefined) {
return compiledMessage;
}
if (
fallbackValue !== undefined
&& typeof fallbackValue === "function"
) {
return fallbackValue as GenericMessageFn;
}
switch (configValue[configMessageTypeSymbol]) {
case configStringSymbol: {
return () =>
createMissingStringFallback(valueMessageIdChain);
}
case configMarkdownSymbol: {
return () =>
createMissingMarkdownFallback(
valueMessageIdChain,
new Set(Object.keys(configValue.slots)),
);
}
}
} else {
const uncompiledSubrecord = (() => {
if (uncompiledValue?.type !== "subrecord") {
errors.push(
`Expected subrecord for key \`${messageId}\`, found: ${typeof uncompiledValue}`,
);
return undefined;
}
return uncompiledValue.subrecord;
})();
return compileSublocale({
subconfig: configValue,
uncompiledSublocale: uncompiledSubrecord,
fallbackSublocale:
typeof fallbackValue === "function" ? undefined : (
fallbackValue
),
errors,
bundle,
messageIdChain: valueMessageIdChain,
});
}
})();
locale[messageId] = compiledValue;
}
// SAFETY: validated above that all keys exist and are the correct type
return { locale: locale as InferLocaleFromConfig<Config>, errors };
}
function fmtMessageIdChain(
messageIdChain: readonly [...string[], string],
): string {
return messageIdChain.join("-");
}
interface CompileMessageCtx {
bundle: FluentBundle;
messageIdChain: readonly [...string[], string];
configValue: ConfigString<object> | ConfigMarkdown<object, object>;
uncompiledMessage: Message;
}
function compileMessage(
ctx: CompileMessageCtx,
): Result<GenericMessageFn, string> {
const { bundle, messageIdChain, configValue, uncompiledMessage } = ctx;
const pattern = uncompiledMessage.value;
if (pattern === null) {
return {
type: "err",
err: `Pattern is null for message with ID: ${fmtMessageIdChain(messageIdChain)}`,
};
}
// validate placeables
if (typeof pattern !== "string") {
for (const element of pattern) {
if (typeof element === "string") {
continue;
}
switch (element.type) {
case "select": {
const { selector } = element;
if (selector.type !== "var") {
return {
type: "err",
err: `Expected selector to be a var expression for ID: ${fmtMessageIdChain(messageIdChain)}; Found: ${selector.type}`,
};
}
if (
!Object.keys(configValue.placeables).includes(
selector.name,
)
) {
return {
type: "err",
err: `Found unexpected placeable name \`${selector.name}\` for ID: ${fmtMessageIdChain(messageIdChain)}`,
};
}
break;
}
case "var": {
if (
!Object.keys(configValue.placeables).includes(
element.name,
)
) {
return {
type: "err",
err: `Found unexpected placeable name \`${element.name}\` for ID: ${fmtMessageIdChain(messageIdChain)}`,
};
}
break;
}
case "term":
case "mesg":
case "func":
case "str":
case "num": {
break; // ignore
}
}
}
}
const allVariantsRes = computeAllVariants(pattern);
if (allVariantsRes.type === "err") {
return {
type: "err",
err: `Failed to compute variants for ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${allVariantsRes.err}`,
};
}
const allVariants = allVariantsRes.ok;
switch (configValue[configMessageTypeSymbol]) {
case configStringSymbol: {
return compileStringMessage({
bundle,
messageIdChain,
allVariants,
pattern,
});
}
case configMarkdownSymbol: {
return compileMarkdownMessage({
bundle,
slots: new Set(Object.keys(configValue.slots)),
messageIdChain,
allVariants,
pattern,
});
}
}
}
interface CompileStringMessageCtx {
bundle: FluentBundle;
messageIdChain: readonly [...string[], string];
allVariants: readonly PatternVariant[];
pattern: Pattern;
}
function compileStringMessage(
ctx: CompileStringMessageCtx,
): Result<GenericStringMessageFn, string> {
const { bundle, messageIdChain, allVariants, pattern } = ctx;
// typecheck string
// check if all variants are valid markdown
for (const variant of allVariants) {
const stringLiteralRes = parseMessageLiteral("string", variant.string);
if (stringLiteralRes.type === "err") {
return {
type: "err",
err: `Invalid literal for variant \`${selectionChainToString(variant.selectionChain)}\` of ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${stringLiteralRes.err}`,
};
}
const stringLiteral = stringLiteralRes.ok;
const stringRes = parseString(stringLiteral);
if (stringRes.type === "err") {
return {
type: "err",
err: `Invalid string for variant \`${selectionChainToString(variant.selectionChain)}\` of ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${stringRes.err}`,
};
}
}
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
return {
type: "ok",
ok: (args: Record<string, PlaceableValue> = {}) => {
const stringRes = ((): Result<string, string> => {
const stringLiteralRes = parseMessageLiteral(
"string",
bundle.formatPattern(pattern, args),
);
if (stringLiteralRes.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid strings above
return {
type: "err",
err: `Failed to parse string literal after compilation!\n${stringLiteralRes.err}`,
};
}
const stringLiteral = stringLiteralRes.ok;
const res = parseString(stringLiteral);
if (res.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid strings above
// TODO: no we dont, do that
return {
type: "err",
err: `Failed to parse string after compilation!\n${res.err}`,
};
}
const string = res.ok;
return { type: "ok", ok: string };
})();
switch (stringRes.type) {
case "ok": {
const string = stringRes.ok;
return string;
}
case "err": {
const error = stringRes.err;
console.error(error);
return createMissingStringFallback(messageIdChain);
}
}
},
};
}
interface CompileMarkdownMessageCtx {
bundle: FluentBundle;
messageIdChain: readonly [...string[], string];
slots: ReadonlySet<string>;
allVariants: readonly PatternVariant[];
pattern: Pattern;
}
function compileMarkdownMessage(
ctx: CompileMarkdownMessageCtx,
): Result<GenericMarkdownMessageFn, string> {
const { bundle, messageIdChain, slots, allVariants, pattern } = ctx;
// typecheck markdown
// check if all variants are valid markdown
for (const variant of allVariants) {
const markdownLiteralRes = parseMessageLiteral("md", variant.string);
if (markdownLiteralRes.type === "err") {
return {
type: "err",
err: `Invalid literal for variant \`${selectionChainToString(variant.selectionChain)}\` of ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${markdownLiteralRes.err}`,
};
}
const markdownLiteral = markdownLiteralRes.ok;
const markdownRes = parseMarkdown(markdownLiteral, slots);
if (markdownRes.type === "err") {
return {
type: "err",
err: `Invalid markdown for variant \`${selectionChainToString(variant.selectionChain)}\` of ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${markdownRes.err}`,
};
}
}
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
return {
type: "ok",
ok: (args: Record<string, PlaceableValue> = {}): Markdown => {
const escapedArgs = Object.fromEntries(
Object.entries(args).map(([id, value]) => {
const escapedValue = (() => {
switch (typeof value) {
case "number": {
return value;
}
case "string": {
return value
.split("")
.map((c) => {
switch (c) {
case "\\": {
return "\\\\";
}
case "*": {
return "\\*";
}
case "#": {
return "\\#";
}
case "[": {
return "\\[";
}
case "]": {
return "\\]";
}
case "(": {
return "\\(";
}
case ")": {
return "\\)";
}
case "-": {
return "\\-";
}
case "<": {
return "\\<";
}
case ">": {
return "\\>";
}
default: {
return c;
}
}
})
.join("");
}
}
})();
return [id, escapedValue] as const;
}),
);
const markdownRes = ((): Result<Markdown, string> => {
const markdownLiteralRes = parseMessageLiteral(
"md",
bundle.formatPattern(pattern, escapedArgs),
);
if (markdownLiteralRes.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid markdown above
return {
type: "err",
err: `Failed to parse markdown literal after compilation!\n${markdownLiteralRes.err}`,
};
}
const markdownLiteral = markdownLiteralRes.ok;
const res = parseMarkdown(markdownLiteral, slots);
if (res.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid markdown above
return {
type: "err",
err: `Failed to parse markdown after compilation!\n${res.err}`,
};
}
return { type: "ok", ok: res.ok };
})();
switch (markdownRes.type) {
case "ok": {
const markdown = markdownRes.ok;
return markdown;
}
case "err": {
const error = markdownRes.err;
console.error(error);
return createMissingMarkdownFallback(messageIdChain, slots);
}
}
},
};
}
function createMissingStringFallback(
messageIdChain: readonly [...string[], string],
): string {
return `[#${fmtMessageIdChain(messageIdChain)}#]`;
}
function createMissingMarkdownFallback<Slot extends string>(
messageIdChain: readonly [...string[], string],
slots: ReadonlySet<Slot>,
): Markdown<Slot> {
return {
elements: [
{
type: "paragraph",
paragraph: {
spans: [
{
type: "plain",
plain: createMissingStringFallback(messageIdChain),
},
],
},
},
],
slots,
};
}
interface CompileSublocaleCtx<Subconfig extends LocaleConfig> {
subconfig: Subconfig;
uncompiledSublocale: UncompiledLocale | undefined;
fallbackSublocale: InferLocaleFromConfig<Subconfig> | undefined;
errors: string[];
bundle: FluentBundle;
messageIdChain: readonly [...string[], string];
}
function compileSublocale<Subconfig extends LocaleConfig>(
ctx: CompileSublocaleCtx<Subconfig>,
): InferLocaleFromConfig<Subconfig> {
const {
subconfig: configValue,
uncompiledSublocale: recordValue,
fallbackSublocale: fallbackValue,
errors,
bundle,
messageIdChain,
} = ctx;
const subrecord = recordValue;
const compiledSubrecordRes = compileLocale({
bundle,
uncompiled: subrecord ?? {},
config: configValue,
fallback: fallbackValue,
messageIdChain,
});
errors.push(
...compiledSubrecordRes.errors.map(
(err) =>
`Error when compiling subrecord with ID: \`${fmtMessageIdChain(messageIdChain)}\`:\n${err}`,
),
);
return compiledSubrecordRes.locale;
}
function parseMessageLiteral(
type: "string" | "md",
message: string,
): Result<string, string> {
const trimmedMessage = message.trim();
const maybeStartIndexes: number[] = [];
const firstQuoteIndex = trimmedMessage.indexOf('"');
if (firstQuoteIndex !== -1) {
maybeStartIndexes.push(firstQuoteIndex);
}
const firstDashIndex = trimmedMessage.indexOf("-");
if (firstDashIndex !== -1) {
maybeStartIndexes.push(firstDashIndex);
}
const stringStartIndex = Math.min(...maybeStartIndexes);
const actualPrefix = trimmedMessage.substring(0, stringStartIndex).trim();
const expectedPrefix = (() => {
switch (type) {
case "string": {
return "";
}
case "md": {
return "md";
}
}
})();
if (actualPrefix !== expectedPrefix) {
return {
type: "err",
err: `Expected prefix "${expectedPrefix}" for message with type \`${type}\`; Found: "${actualPrefix}"`,
};
}
return {
type: "ok",
ok: trimmedMessage.substring(actualPrefix.length).trim(),
};
}
function parseString(message: string): Result<string, string> {
const AFFIX = '"';
if (!message.startsWith(AFFIX)) {
return {
type: "err",
err: `String message expected to start with \`${AFFIX}\``,
};
}
if (!message.endsWith(AFFIX)) {
return {
type: "err",
err: `String message expected to end with \`${AFFIX}\``,
};
}
const deprefixed = message.substring(AFFIX.length);
const dequoted = deprefixed.substring(0, deprefixed.length - AFFIX.length);
return { type: "ok", ok: dequoted };
}