Improve i18n support and config validation
This commit is contained in:
parent
4f83ae8802
commit
37db38b613
9 changed files with 152 additions and 152 deletions
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue