Add static checking to i18n and improve types
This commit is contained in:
parent
f8ac450ab5
commit
579e5879e1
16 changed files with 126 additions and 82 deletions
12
README.md
12
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
|
||||
```
|
||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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 --",
|
||||
|
|
|
@ -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> = {};
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}"`);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]!];
|
||||
|
|
|
@ -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]!];
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)}`,
|
||||
),
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
12
src/utils/get_username_for_lang.ts
Normal file
12
src/utils/get_username_for_lang.ts
Normal file
|
@ -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;
|
||||
}
|
Loading…
Add table
Reference in a new issue