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;
+}