diff --git a/README.md b/README.md index 39f8d5e..3fd7a82 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Static website built in Astro + Typescript + TailwindCSS. ## Requirements - Node.js 20+ -- LFTP, for remote deployment script -- LibreOffice, for story export script +- (optional) LFTP, for the remote deployment script. +- (optional) LibreOffice, for the story export script. ## Development @@ -42,7 +42,7 @@ npm run build Then, if you're using LFTP: -1. Create a new `.env` file at the root of the project: +1. Create a new `.env` file at the root of the project with your credentials (SSH, SFTP, WebDav, etc.) if you haven't already: ```env DEPLOY_LFTP_HOST=https://example-webdav-server.com @@ -51,6 +51,8 @@ DEPLOY_LFTP_PASSWORD=sup3r_s3cr3t_password DEPLOY_LFTP_TARGETFOLDER=sites/gallery.badmanners.xyz/ ``` -2. Run the following command: `npm run deploy-lftp` +2. Run the deploy command: -Otherwise, to deploy over SSH: `scp -r ./dist/* my-ssh-server:./gallery.badmanners.xyz/` +```bash +npm run deploy-lftp +``` diff --git a/package-lock.json b/package-lock.json index be9e0ea..1be4ca0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gallery-badmanners-xyz", - "version": "1.5.3", + "version": "1.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gallery-badmanners-xyz", - "version": "1.5.3", + "version": "1.5.4", "dependencies": { "@astrojs/check": "^0.8.2", "@astrojs/rss": "^4.0.7", diff --git a/package.json b/package.json index 26cb2eb..6084abb 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "gallery-badmanners-xyz", "type": "module", - "version": "1.5.3", + "version": "1.5.4", "scripts": { "dev": "astro dev", "start": "astro dev", - "build": "astro check --minimumSeverity warning && astro build", + "build": "npm run check && astro build", "preview": "astro preview", "sync": "astro sync", + "check": "astro check --minimumSeverity warning", "astro": "astro", "prettier": "prettier --write .", "deploy-lftp": "dotenv tsx scripts/deploy-lftp.ts --", diff --git a/src/components/MastodonComments.astro b/src/components/MastodonComments.astro index fd8fbbc..a9eff55 100644 --- a/src/components/MastodonComments.astro +++ b/src/components/MastodonComments.astro @@ -140,7 +140,6 @@ const { instance, user, postId } = Astro.props; throw new Error(`Received error status ${response.status} - ${response.statusText}!`); } const data: { ancestors: Comment[]; descendants: Comment[] } = await response.json(); - // console.log(data); const commentsList: HTMLElement[] = []; const commentMap: Record<string, number> = {}; diff --git a/src/components/UserComponent.astro b/src/components/UserComponent.astro index 9f6ab8e..e8cd6d3 100644 --- a/src/components/UserComponent.astro +++ b/src/components/UserComponent.astro @@ -1,7 +1,7 @@ --- import { type CollectionEntry, getEntry } from "astro:content"; -import { t } from "../i18n"; import { type Lang } from "../content/config"; +import { getUsernameForLang } from "../utils/get_username_for_lang"; type Props = { lang: Lang; @@ -12,7 +12,7 @@ let { user, lang } = Astro.props; if (user.data.isAnonymous) { user = await getEntry("users", "anonymous"); } -const username = t(lang, user.data.nameLang as any) || user.data.name; +const username = getUsernameForLang(user, lang); let link: string | null = null; if (user.data.preferredLink) { const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string]; diff --git a/src/content/config.ts b/src/content/config.ts index a506fa6..7993dfe 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -1,7 +1,6 @@ import { defineCollection, reference, z } from "astro:content"; -export const adjustDateForUTCOffset = (date: Date) => - new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0); +// Constants export const WEBSITE_LIST = [ "website", @@ -15,6 +14,10 @@ export const WEBSITE_LIST = [ "bluesky", "itaku", ] as const; +export const GAME_PLATFORMS = ["web", "windows", "linux", "macos", "android", "ios"] as const; +export const DEFAULT_LANG = "eng"; + +// Validators const ekaPostUrlRegex = /^(?:https:\/\/)(?:www\.)?aryion\.com\/g4\/view\/([1-9]\d*)\/?$/; const furaffinityPostUrlRegex = /^(?:https:\/\/)(?:www\.)?furaffinity\.net\/view\/([1-9]\d*)\/?$/; @@ -27,9 +30,16 @@ const mastodonPostUrlRegex = /^(?:https:\/\/)((?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/@([a const refineAuthors = (value: { id: any } | any[]) => "id" in value || value.length > 0; const refineCopyrightedCharacters = (value: Record<string, any>) => !("" in value) || Object.keys(value).length == 1; -const lang = z.enum(["eng", "tok"]).default("eng"); +// Transformers + +export const adjustDateForUTCOffset = (date: Date) => + new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0); + +// Types + +const lang = z.enum(["eng", "tok"]).default(DEFAULT_LANG); const website = z.enum(WEBSITE_LIST); -const platform = z.enum(["web", "windows", "linux", "macos", "android", "ios"]); +const platform = z.enum(GAME_PLATFORMS); const mastodonPost = z .object({ instance: z.string(), @@ -60,8 +70,11 @@ const copyrightedCharacters = z export type Lang = z.output<typeof lang>; export type Website = z.infer<typeof website>; +export type GamePlatform = z.infer<typeof platform>; export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>; +// Content collections + const storiesCollection = defineCollection({ type: "content", schema: ({ image }) => @@ -144,6 +157,8 @@ const gamesCollection = defineCollection({ }), }); +// Data collections + const usersCollection = defineCollection({ type: "data", schema: ({ image }) => @@ -186,7 +201,7 @@ const tagCategoriesCollection = defineCollection({ z.object({ name: z.union([ z.string(), - z.record(lang, z.string()).refine((tag) => "eng" in tag, `object-formatted tag must have an "eng" key`), + z.intersection(z.object({ [DEFAULT_LANG]: z.string() }), z.record(lang, z.string())), ]), description: z.string().optional(), related: z.array(z.string()).optional(), @@ -196,10 +211,8 @@ const tagCategoriesCollection = defineCollection({ }); export const collections = { - // Content collections stories: storiesCollection, games: gamesCollection, - // Data collections users: usersCollection, series: seriesCollection, "tag-categories": tagCategoriesCollection, diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 143c6f0..4826028 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,14 +1,8 @@ -import { type Lang } from "../content/config"; +import { type GamePlatform, type Lang } from "../content/config"; +import { DEFAULT_LANG } from "../content/config"; +export { DEFAULT_LANG } from "../content/config"; -export const DEFAULT_LANG = "eng" satisfies Lang; - -type Translation = string | ((...args: any[]) => string); - -export type TranslationRecord = { [DEFAULT_LANG]: Translation } & { - [L in Exclude<Lang, typeof DEFAULT_LANG>]?: Translation; -}; - -export const UI_STRINGS: Record<string, TranslationRecord> = { +export const UI_STRINGS = { "util/join_names": { eng: (names: string[]) => names.length <= 1 @@ -90,11 +84,10 @@ export const UI_STRINGS: Record<string, TranslationRecord> = { tok: "lipu lawa", }, "story/authors": { - eng: (authorsList: string[]) => - `by ${(UI_STRINGS["util/join_names"]!.eng as (arg: string[]) => string)(authorsList)}`, + eng: (authorsList: string[]) => `by ${UI_STRINGS["util/join_names"].eng(authorsList)}`, tok: (authorsList: string[]) => authorsList.length > 1 - ? `lipu ni li tan jan ni: ${(UI_STRINGS["util/join_names"]!.tok as (arg: string[]) => string)(authorsList)}` + ? `lipu ni li tan jan ni: ${UI_STRINGS["util/join_names"].tok(authorsList)}` : `lipu ni li tan ${authorsList[0]}`, }, "story/commissioned_by": { @@ -102,7 +95,7 @@ export const UI_STRINGS: Record<string, TranslationRecord> = { if (typeof commissionersList === "string") { commissionersList = [commissionersList]; } - return `Commissioned by ${(UI_STRINGS["util/join_names"]!.eng as (arg: string[]) => string)(commissionersList)}`; + return `Commissioned by ${UI_STRINGS["util/join_names"].eng(commissionersList)}`; }, }, "story/requested_by": { @@ -110,7 +103,7 @@ export const UI_STRINGS: Record<string, TranslationRecord> = { if (typeof requestersList === "string") { requestersList = [requestersList]; } - return `Requested by ${(UI_STRINGS["util/join_names"]!.eng as (arg: string[]) => string)(requestersList)}`; + return `Requested by ${UI_STRINGS["util/join_names"].eng(requestersList)}`; }, }, "story/draft_warning": { @@ -120,17 +113,17 @@ export const UI_STRINGS: Record<string, TranslationRecord> = { eng: (owner: string, charactersList: string[]) => charactersList.length == 1 ? `${charactersList[0]} is © ${owner}` - : `${(UI_STRINGS["util/join_names"]!.eng as (arg: string[]) => string)(charactersList)} are © ${owner}`, + : `${UI_STRINGS["util/join_names"].eng(charactersList)} are © ${owner}`, }, "characters/all_characters_are_copyrighted_by": { eng: (owner: string) => `All characters are © ${owner}`, }, "game/platforms": { - eng: (platforms: string[]) => { + eng: (platforms: GamePlatform[]) => { const translatedPlatforms = platforms.map( - (platform) => (UI_STRINGS[`game/platform_${platform}`]?.eng as string | undefined) || platform, + (platform) => (UI_STRINGS[`game/platform_${platform}`].eng as string | undefined) || platform, ); - return `A game for ${(UI_STRINGS["util/join_names"]!.eng as (arg: string[]) => string)(translatedPlatforms)}`; + return `A game for ${UI_STRINGS["util/join_names"].eng(translatedPlatforms)}`; }, }, "game/platform_web": { @@ -152,27 +145,25 @@ export const UI_STRINGS: Record<string, TranslationRecord> = { eng: "iOS", }, "game/warnings": { - eng: (platforms: string[], contentWarning: string) => - `${(UI_STRINGS["game/platforms"]!.eng as (arg: string[]) => string)(platforms)}. ${contentWarning}`, + eng: (platforms: GamePlatform[], contentWarning: string) => + `${UI_STRINGS["game/platforms"].eng(platforms)}. ${contentWarning}`, }, -}; +} as const; -export function t(lang: Lang, stringOrSource: string | TranslationRecord, ...args: any[]): string { - if (typeof stringOrSource === "object") { - const translation = stringOrSource[lang] || stringOrSource[DEFAULT_LANG]; - if (typeof translation === "function") { - return translation(...args); - } - return translation; +type TranslationKey = keyof typeof UI_STRINGS; +type Translation<A extends any[]> = string | ((...args: A) => string); +type TranslationArgs<T extends Translation<any[]>> = T extends (...args: infer A) => string ? A : []; +type TranslationEntry<T extends Translation<any[]>> = { [DEFAULT_LANG]: T } & { + [L in Exclude<Lang, typeof DEFAULT_LANG>]?: T; +}; +type TranslationKeyArgs<K extends TranslationKey> = + (typeof UI_STRINGS)[K] extends TranslationEntry<infer T> ? TranslationArgs<T> : never; + +export function t<K extends TranslationKey>(lang: Lang, key: K, ...args: TranslationKeyArgs<K>): string { + if (key in UI_STRINGS) { + const translation: Translation<TranslationKeyArgs<K>> = + (UI_STRINGS[key] as any)[lang] || UI_STRINGS[key][DEFAULT_LANG]; + return typeof translation === "function" ? translation(...args) : translation; } - if (UI_STRINGS[stringOrSource]) { - const translation = UI_STRINGS[stringOrSource][lang] || UI_STRINGS[stringOrSource][DEFAULT_LANG]; - if (typeof translation === "function") { - return translation(...args); - } - return translation; - } - // console.warn(`No translation map found for "${stringOrSource}"`); - // return stringOrSource; - throw new Error(`No translation map found for "${stringOrSource}"`); + throw new Error(`No translation map found for "${key}"`); } diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 7dd5d41..6179484 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -25,7 +25,12 @@ const { pageTitle } = Astro.props; <meta name="generator" content={Astro.generator} /> <title>{pageTitle || "Gallery"} | Bad Manners</title> <link rel="me" href="https://meow.social/@BadManners" /> - <link rel="alternate" type="application/rss+xml" title="Gallery | Bad Manners" href={`${Astro.site}feed.xml`} /> + <link + rel="alternate" + type="application/rss+xml" + title="Gallery | Bad Manners" + href={new URL("/feed.xml", Astro.site)} + /> <slot name="head" /> </head> <body> diff --git a/src/layouts/GameLayout.astro b/src/layouts/GameLayout.astro index 7f360ee..2bf2d7a 100644 --- a/src/layouts/GameLayout.astro +++ b/src/layouts/GameLayout.astro @@ -22,15 +22,19 @@ const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrighte // const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft); const categorizedTags = Object.fromEntries( (await getCollection("tag-categories")).flatMap((category) => - category.data.tags.map<[string, string]>(({ name }) => - typeof name === "string" ? [name, name] : [t(DEFAULT_LANG, name as any), t(props.lang, name as any)], + category.data.tags.map<[string, string | null]>(({ name }) => + typeof name === "string" ? [name, name] : [name[DEFAULT_LANG], name[props.lang] ?? null], ), ), ); const tags = props.tags.map<[string, string]>((tag) => { const tagSlug = slug(tag); if (!(tag in categorizedTags)) { - console.log(`Tag "${tag}" doesn't have a category in the "tag-categories" collection!`); + console.warn(`Tag "${tag}" doesn't have a category in the "tag-categories" collection`); + return [tagSlug, tag]; + } + if (categorizedTags[tag] == null) { + console.warn(`No "${props.lang}" translation for tag "${tag}"`); return [tagSlug, tag]; } return [tagSlug, categorizedTags[tag]!]; diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro index a5c8e71..a820caf 100644 --- a/src/layouts/StoryLayout.astro +++ b/src/layouts/StoryLayout.astro @@ -34,15 +34,19 @@ const relatedStories = (await getEntries(props.relatedStories)).filter((story) = // const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft); const categorizedTags = Object.fromEntries( (await getCollection("tag-categories")).flatMap((category) => - category.data.tags.map<[string, string]>(({ name }) => - typeof name === "string" ? [name, name] : [t(DEFAULT_LANG, name as any), t(props.lang, name as any)], + category.data.tags.map<[string, string | null]>(({ name }) => + typeof name === "string" ? [name, name] : [name[DEFAULT_LANG], name[props.lang] ?? null], ), ), ); const tags = props.tags.map<[string, string]>((tag) => { const tagSlug = slug(tag); if (!(tag in categorizedTags)) { - console.log(`Tag "${tag}" doesn't have a category in the "tag-categories" collection!`); + console.warn(`Tag "${tag}" doesn't have a category in the "tag-categories" collection`); + return [tagSlug, tag]; + } + if (categorizedTags[tag] == null) { + console.warn(`No "${props.lang}" translation for tag "${tag}"`); return [tagSlug, tag]; } return [tagSlug, categorizedTags[tag]!]; diff --git a/src/pages/api/export-story/[...slug].ts b/src/pages/api/export-story/[...slug].ts index 05473fe..431549f 100644 --- a/src/pages/api/export-story/[...slug].ts +++ b/src/pages/api/export-story/[...slug].ts @@ -4,6 +4,7 @@ import type { Lang, Website } from "../../../content/config"; import { t } from "../../../i18n"; import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters"; import { markdownToBbcode } from "../../../utils/markdown_to_bbcode"; +import { getUsernameForLang } from "../../../utils/get_username_for_lang"; interface ExportWebsiteInfo { website: Website; @@ -156,9 +157,9 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa function getNameForUser(user: CollectionEntry<"users">, anonymousUser: CollectionEntry<"users">, lang: Lang): string { if (user.data.isAnonymous) { - return t(lang, anonymousUser.data.nameLang as any) || anonymousUser.data.name; + return getUsernameForLang(anonymousUser, lang); } - return t(lang, user.data.nameLang as any) || user.data.name; + return getUsernameForLang(user, lang); } type Props = { @@ -179,14 +180,15 @@ export const getStaticPaths: GetStaticPaths = async () => { })); }; +const ANONYMOUS_USER = await getEntry("users", "anonymous"); + export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) => { const { lang } = story.data; const copyrightedCharacters = await formatCopyrightedCharacters(story.data.copyrightedCharacters); 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 anonymousFallback = getNameForUser(ANONYMOUS_USER, ANONYMOUS_USER, lang); const description = Object.fromEntries( WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => { @@ -234,10 +236,10 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) = `${t( lang, "story/authors", - authorsList.map((author) => getNameForUser(author, anonymousUser, lang)), + authorsList.map((author) => getNameForUser(author, ANONYMOUS_USER, lang)), )}\n` + - (commissioner ? `${t(lang, "story/commissioned_by", getNameForUser(commissioner, anonymousUser, lang))}\n` : "") + - (requester ? `${t(lang, "story/requested_by", getNameForUser(requester, anonymousUser, lang))}\n` : ""); + (commissioner ? `${t(lang, "story/commissioned_by", getNameForUser(commissioner, ANONYMOUS_USER, lang))}\n` : "") + + (requester ? `${t(lang, "story/requested_by", getNameForUser(requester, ANONYMOUS_USER, lang))}\n` : ""); const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}` .replaceAll(/\n\n\n+/g, "\n\n") diff --git a/src/pages/feed.xml.ts b/src/pages/feed.xml.ts index edd8510..2674c76 100644 --- a/src/pages/feed.xml.ts +++ b/src/pages/feed.xml.ts @@ -6,6 +6,7 @@ import sanitizeHtml from "sanitize-html"; import { t } from "../i18n"; import type { Lang } from "../content/config"; import { markdownToPlaintext } from "../utils/markdown_to_plaintext"; +import { getUsernameForLang } from "../utils/get_username_for_lang"; type FeedItem = RSSFeedItem & { pubDate: Date; @@ -20,7 +21,7 @@ function toNoonUTCDate(date: Date) { } const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => { - const userName = user.data.nameLang[lang] || user.data.name; + const userName = getUsernameForLang(user, lang); if (user.data.preferredLink) { const link = user.data.links[user.data.preferredLink]!; return `<a href="${typeof link === "string" ? link : link[0]}">${userName}</a>`; @@ -40,7 +41,7 @@ export const GET: APIRoute = async ({ site }) => { return rss({ title: "Gallery | Bad Manners", description: "Stories, games, and (possibly) more by Bad Manners", - site: site as URL, + site: site!, items: [ await Promise.all( stories.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({ @@ -48,7 +49,7 @@ export const GET: APIRoute = async ({ site }) => { pubDate: toNoonUTCDate(data.pubDate!), link: `/stories/${slug}`, description: - `${t(data.lang, "story/warnings", data.wordCount, data.contentWarning.trim())}\n\n${markdownToPlaintext(data.description)}` + `${t(data.lang, "story/warnings", data.wordCount || "???", data.contentWarning.trim())}\n\n${markdownToPlaintext(data.description)}` .replaceAll(/[\n ]+/g, " ") .trim(), categories: ["story"], @@ -72,7 +73,7 @@ export const GET: APIRoute = async ({ site }) => { (data.commissioner ? `<p>${t(data.lang, "export_story/commissioned_by", getLinkForUser(users.find((user) => user.id === data.commissioner!.id)!, data.lang))}</p>` : "") + - `<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning.trim())}</em></p>` + + `<hr><p><em>${t(data.lang, "story/warnings", data.wordCount || "???", data.contentWarning.trim())}</em></p>` + `<hr>${await markdown(body)}` + `<hr>${await markdown(data.description)}`, ), diff --git a/src/pages/index.astro b/src/pages/index.astro index 2ac7fca..5640b76 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -28,7 +28,7 @@ const latestItems: LatestItemsEntry[] = [ thumbnail: story.data.thumbnail, href: `/stories/${story.slug}`, title: story.data.title, - altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim()), + altText: t(story.data.lang, "story/warnings", story.data.wordCount || "???", story.data.contentWarning.trim()), pubDate: story.data.pubDate!, })), games.map<LatestItemsEntry>((game) => ({ diff --git a/src/pages/stories/[page].astro b/src/pages/stories/[page].astro index 4ab3aa7..843a966 100644 --- a/src/pages/stories/[page].astro +++ b/src/pages/stories/[page].astro @@ -28,8 +28,8 @@ const totalPages = Math.ceil(page.total / page.size); <p class="text-center font-light text-stone-950 dark:text-white"> { page.start == page.end - ? `Displaying story ${page.start + 1}` - : `Displaying stories ${page.start + 1} - ${page.end + 1}` + ? `Displaying story #${page.start + 1}` + : `Displaying stories #${page.start + 1}–${page.end + 1}` } / {page.total} </p> <div class="mx-auto mb-6 mt-2 flex w-fit rounded-lg border border-stone-400 dark:border-stone-500"> @@ -71,7 +71,12 @@ const totalPages = Math.ceil(page.total / page.size); <a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`} - title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())} + title={t( + story.data.lang, + "story/warnings", + story.data.wordCount || "???", + story.data.contentWarning.trim(), + )} > {story.data.thumbnail ? ( <div class="flex aspect-square max-w-[192px] justify-center"> diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro index fc894dc..c49006d 100644 --- a/src/pages/tags/[slug].astro +++ b/src/pages/tags/[slug].astro @@ -85,7 +85,7 @@ export const getStaticPaths: GetStaticPaths = async () => { const { tag, description, stories, games, related } = Astro.props; if (!description) { - console.warn(`Tag "${tag}" has no description!`); + console.log(`Tag "${tag}" has no description`); } const count = stories.length + games.length; let totalWorksWithTag: string = ""; @@ -132,7 +132,12 @@ if (count == 1) { <a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`} - title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())} + title={t( + story.data.lang, + "story/warnings", + story.data.wordCount || "???", + story.data.contentWarning.trim(), + )} > {story.data.thumbnail ? ( <div class="flex aspect-square max-w-[192px] justify-center"> diff --git a/src/utils/get_username_for_lang.ts b/src/utils/get_username_for_lang.ts new file mode 100644 index 0000000..df55190 --- /dev/null +++ b/src/utils/get_username_for_lang.ts @@ -0,0 +1,12 @@ +import type { CollectionEntry } from "astro:content"; +import type { Lang } from "../content/config"; + +export function getUsernameForLang(user: CollectionEntry<"users">, lang: Lang): string { + if (user.data.nameLang) { + if (user.data.nameLang[lang]) { + return user.data.nameLang[lang]; + } + throw new Error(`No "${lang}" translation for username "${user.data.name}" ("${user.id}")`); + } + return user.data.name; +}