Improve i18n support and config validation

This commit is contained in:
Bad Manners 2024-03-30 15:04:29 -03:00
parent 4f83ae8802
commit 37db38b613
9 changed files with 152 additions and 152 deletions

View file

@ -5,7 +5,7 @@ import { decode as tinyDecode } from "tiny-decode";
import { type Lang, type Website } from "../../../content/config";
import { t } from "../../../i18n";
type DescriptionFormat = "bbcode" | "markdown";
type ExportFormat = "bbcode" | "markdown";
const WEBSITE_LIST = [
["eka", "bbcode"],
@ -13,9 +13,9 @@ const WEBSITE_LIST = [
["inkbunny", "bbcode"],
["sofurry", "bbcode"],
["weasyl", "markdown"],
] as const satisfies [Website, DescriptionFormat][];
] as const satisfies [Website, ExportFormat][];
type ExportWebsite = typeof WEBSITE_LIST extends ReadonlyArray<[infer K, DescriptionFormat]> ? K : never;
type ExportWebsite = typeof WEBSITE_LIST extends ReadonlyArray<[infer K, ExportFormat]> ? K : never;
const bbcodeRenderer: RendererApi = {
strong: (text) => `[b]${text}[/b]`,
@ -108,7 +108,7 @@ function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website)
}
break;
default:
throw new Error(`Unhandled website "${website}" in getUsernameForWebsite`);
throw new Error(`Unhandled Website "${website}"`);
}
} else {
return link[1].replace(/^@/, "");
@ -117,24 +117,14 @@ function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website)
throw new Error(`Cannot get "${website}" username for user "${user.id}"`);
}
function isPreferredWebsite(
user: CollectionEntry<"users">,
website: Website,
preferredChoices: readonly Website[],
): boolean {
const { preferredLink, links } = user.data;
if (!(website in links)) {
return false;
}
if (!preferredLink || preferredLink == website) {
return true;
}
return !preferredChoices.includes(preferredLink);
function isPreferredWebsite(user: CollectionEntry<"users">, website: Website): boolean {
const { preferredLink } = user.data;
return !preferredLink || preferredLink == website;
}
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsite): string {
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsite, anonymousFallback: string): string {
if (user.data.isAnonymous) {
return "anonymous";
return anonymousFallback;
}
switch (website) {
case "eka":
@ -148,51 +138,46 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsite):
}
break;
case "weasyl":
const weasylPreferredWebsites = ["furaffinity", "inkbunny", "sofurry"] as const;
if ("weasyl" in user.data.links) {
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
} else if (isPreferredWebsite(user, "furaffinity", weasylPreferredWebsites)) {
} else if (isPreferredWebsite(user, "furaffinity")) {
return `<fa:${getUsernameForWebsite(user, "furaffinity")}>`;
} else if (isPreferredWebsite(user, "inkbunny", weasylPreferredWebsites)) {
} else if (isPreferredWebsite(user, "inkbunny")) {
return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`;
} else if (isPreferredWebsite(user, "sofurry", weasylPreferredWebsites)) {
} else if (isPreferredWebsite(user, "sofurry")) {
return `<sf:${getUsernameForWebsite(user, "sofurry")}>`;
}
break;
case "inkbunny":
const inkbunnyPreferredWebsites = ["furaffinity", "sofurry", "weasyl"] as const;
if ("inkbunny" in user.data.links) {
return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`;
} else if (isPreferredWebsite(user, "furaffinity", inkbunnyPreferredWebsites)) {
} else if (isPreferredWebsite(user, "furaffinity")) {
return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`;
} else if (isPreferredWebsite(user, "sofurry", inkbunnyPreferredWebsites)) {
} else if (isPreferredWebsite(user, "sofurry")) {
return `[sf]${getUsernameForWebsite(user, "sofurry")}[/sf]`;
} else if (isPreferredWebsite(user, "weasyl", inkbunnyPreferredWebsites)) {
} else if (isPreferredWebsite(user, "weasyl")) {
return `[weasyl]${getUsernameForWebsite(user, "weasyl")}[/weasyl]`;
}
break;
case "sofurry":
const sofurryPreferredWebsites = ["furaffinity", "inkbunny"] as const;
if ("sofurry" in user.data.links) {
return `:icon${getUsernameForWebsite(user, "sofurry")}:`;
} else if (isPreferredWebsite(user, "furaffinity", sofurryPreferredWebsites)) {
} else if (isPreferredWebsite(user, "furaffinity")) {
return `fa!${getUsernameForWebsite(user, "furaffinity")}`;
} else if (isPreferredWebsite(user, "inkbunny", sofurryPreferredWebsites)) {
} else if (isPreferredWebsite(user, "inkbunny")) {
return `ib!${getUsernameForWebsite(user, "inkbunny")}`;
}
break;
default:
throw new Error(`Unhandled website "${website}" in getLinkForUser`);
throw new Error(`Unhandled ExportWebsite "${website}"`);
}
if (user.data.preferredLink) {
if (user.data.preferredLink in user.data.links) {
const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string];
return `[${user.data.name}](${typeof preferredLink === "string" ? preferredLink : preferredLink[0]})`;
} else {
throw new Error(`No preferredLink "${user.data.preferredLink}" for user ${user.id}`);
}
const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string];
return `[${user.data.name}](${typeof preferredLink === "string" ? preferredLink : preferredLink[0]})`;
}
throw new Error(`No "${website}" link for user "${user.id}" (consider setting preferredLink)`);
throw new Error(
`No matching "${website}" link for user "${user.id}" (consider setting their "preferredLink" property)`,
);
}
function getNameForUser(user: CollectionEntry<"users">, anonymousUser: CollectionEntry<"users">, lang: Lang): string {
@ -222,55 +207,47 @@ export const getStaticPaths: GetStaticPaths = async () => {
export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) => {
const { lang } = story.data;
if (
story.data.copyrightedCharacters &&
"" in story.data.copyrightedCharacters &&
Object.keys(story.data.copyrightedCharacters).length > 1
) {
throw new Error("copyrightedCharacter cannot use empty key (catch-all) with other keys");
}
const charactersPerUser =
story.data.copyrightedCharacters &&
Object.keys(story.data.copyrightedCharacters).reduce(
(acc, character) => {
const key = story.data.copyrightedCharacters[character].id;
if (!(key in acc)) {
acc[key] = [];
}
acc[key].push(character);
return acc;
},
{} as Record<
CollectionEntry<"users">["id"],
(typeof story.data.copyrightedCharacters extends Record<infer K, any> ? K : never)[]
>,
);
const copyrightedCharacters = await Promise.all(
Object.values(
Object.keys(story.data.copyrightedCharacters).reduce(
(acc, character) => {
const user = story.data.copyrightedCharacters[character];
if (!(user.id in acc)) {
acc[user.id] = [getEntry(user), []];
}
acc[user.id][1].push(character);
return acc;
},
{} as Record<string, [Promise<CollectionEntry<"users">>, string[]]>,
),
).map(async ([userPromise, characters]) => [await userPromise, characters] as [CollectionEntry<"users">, string[]]),
);
const authorsList = await getEntries([story.data.authors].flat());
const commissioner = story.data.commissioner && (await getEntry(story.data.commissioner));
const requester = story.data.requester && (await getEntry(story.data.requester));
const anonymousUser = await getEntry("users", "anonymous");
const anonymousFallback = getNameForUser(anonymousUser, anonymousUser, lang);
const description: Record<ExportWebsite, string> = Object.fromEntries(
await Promise.all(
WEBSITE_LIST.map(async ([website, exportFormat]) => {
const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website);
const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website, anonymousFallback);
const storyDescription = (
[
story.data.description,
`*Word count: ${story.data.wordCount}. ${story.data.contentWarning.trim()}*`,
"Writing: " + (await getEntries([story.data.authors].flat())).map((author) => u(author)).join(" "),
story.data.requester && "Request for: " + u(await getEntry(story.data.requester)),
story.data.commissioner && "Commissioned by: " + u(await getEntry(story.data.commissioner)),
...(await Promise.all(
(Object.keys(charactersPerUser) as CollectionEntry<"users">["id"][]).map(async (id) => {
const user = u(await getEntry("users", id));
const characterList = charactersPerUser[id];
if (characterList[0] == "") {
return `All characters are © ${user}`;
} else if (characterList.length > 2) {
return `${characterList.slice(0, characterList.length - 1).join(", ")}, and ${characterList[characterList.length - 1]} are © ${user}`;
} else if (characterList.length > 1) {
return `${characterList[0]} and ${characterList[1]} are © ${user}`;
}
return `${characterList[0]} is © ${user}`;
}),
)),
t(lang, "export_story/warnings", story.data.wordCount, story.data.contentWarning.trim()),
t(
lang,
"export_story/writing",
authorsList.map((author) => u(author)),
),
requester && t(lang, "export_story/request_for", u(requester)),
commissioner && t(lang, "export_story/commissioned_by", u(commissioner)),
...copyrightedCharacters.map(([user, characterList]) =>
characterList[0] == ""
? t(lang, "characters/all_characters_are_copyrighted_by", u(user))
: t(lang, "characters/characters_are_copyrighted_by", u(user), characterList),
),
].filter((data) => data) as string[]
)
.join("\n\n")
@ -290,25 +267,22 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
if (exportFormat === "markdown") {
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
}
throw new Error(`Unknown exportFormat "${exportFormat}"`);
throw new Error(`Unhandled ExportFormat "${exportFormat}"`);
}),
),
);
const anonymousUser = await getEntry("users", "anonymous");
const authorsNames = (await getEntries([story.data.authors].flat())).map((author) =>
getNameForUser(author, anonymousUser, lang),
);
const commissioner = story.data.commissioner && (await getEntry(story.data.commissioner));
const requester = story.data.requester && (await getEntry(story.data.requester));
const storyHeader =
`${story.data.title}\n` +
`${t(lang, "story/authors", authorsNames)}\n` +
`${t(
lang,
"story/authors",
authorsList.map((author) => getNameForUser(author, anonymousUser, lang)),
)}\n` +
(commissioner ? `${t(lang, "story/commissioned_by", getNameForUser(commissioner, anonymousUser, lang))}\n` : "") +
(requester ? `${t(lang, "story/requested_by", getNameForUser(requester, anonymousUser, lang))}\n` : "");
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll("\\*", "*").replaceAll("\\=", "=")}`
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}`
.replaceAll(/\n\n\n+/g, "\n\n")
.trim();

View file

@ -52,9 +52,6 @@ const categorizedTags: Array<[string, string, string[]]> = tagCategories
if (typeof tag === "string") {
return tag;
}
if (!("eng" in tag)) {
throw new Error(`Object-formatted tag must have an "eng" key: ${tag}`);
}
return tag["eng"]!;
});
tagList.forEach((tag, index) => {