Add i18n module and fix missing CopyrightedCharacters
This commit is contained in:
parent
7ca6f52cc2
commit
4f83ae8802
11 changed files with 270 additions and 196 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "gallery-badmanners-xyz",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gallery-badmanners-xyz",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.5.9",
|
||||
"@astrojs/rss": "^4.0.5",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "gallery-badmanners-xyz",
|
||||
"type": "module",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
|
|
|
@ -1,58 +1,15 @@
|
|||
---
|
||||
import { type CollectionEntry } from "astro:content";
|
||||
import { type Lang } from "../content/config";
|
||||
import UserComponent from "./UserComponent.astro";
|
||||
import { t } from "../i18n";
|
||||
|
||||
type Props = {
|
||||
authors: CollectionEntry<"users"> | CollectionEntry<"users">[];
|
||||
lang: Lang;
|
||||
};
|
||||
|
||||
const { authors, lang } = Astro.props;
|
||||
const authorsArray = [authors].flat();
|
||||
const { lang } = Astro.props;
|
||||
const authors = Astro.slots.has("default")
|
||||
? (await Astro.slots.render("default")).replaceAll(/\<\/(a|span)\>\</g, "</$1><br><")
|
||||
: "";
|
||||
---
|
||||
|
||||
{
|
||||
authorsArray.length > 0 ? (
|
||||
<p class="font-light">
|
||||
{lang === "eng" &&
|
||||
(authorsArray.length > 2 ? (
|
||||
<span>
|
||||
by{" "}
|
||||
{authorsArray.slice(0, authorsArray.length - 1).map((author) => (
|
||||
<Fragment>
|
||||
<UserComponent lang="eng" user={author} />,
|
||||
</Fragment>
|
||||
))}
|
||||
and <UserComponent lang="eng" user={authorsArray[authorsArray.length - 1]} />
|
||||
</span>
|
||||
) : authorsArray.length > 1 ? (
|
||||
<span>
|
||||
by <UserComponent lang="eng" user={authorsArray[0]} /> and{" "}
|
||||
<UserComponent lang="eng" user={authorsArray[1]} />
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
by <UserComponent lang="eng" user={authorsArray[0]} />
|
||||
</span>
|
||||
))}
|
||||
{lang === "tok" &&
|
||||
(authorsArray.length > 1 ? (
|
||||
<span>
|
||||
lipu ni li tan jan ni:{" "}
|
||||
{authorsArray.slice(0, authorsArray.length - 1).map((author) => (
|
||||
<Fragment>
|
||||
<UserComponent lang="tok" user={author} />
|
||||
{" en "}
|
||||
</Fragment>
|
||||
))}
|
||||
<UserComponent lang="tok" user={authorsArray[authorsArray.length - 1]} />
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
lipu ni li tan <UserComponent lang="tok" user={authorsArray[0]} />
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
{authors ? <p id="authors" set:html={t(lang, "story/authors", authors.split("<br>"))} /> : null}
|
||||
|
|
|
@ -1,67 +1,34 @@
|
|||
---
|
||||
import { type CollectionEntry } from "astro:content";
|
||||
import { type Lang } from "../content/config";
|
||||
import { t } from "../i18n";
|
||||
import UserComponent from "./UserComponent.astro";
|
||||
import CopyrightedCharactersItem from "./CopyrightedCharactersItem.astro";
|
||||
|
||||
type Props = {
|
||||
copyrightedCharacters?: Record<string, CollectionEntry<"users">>;
|
||||
copyrightedCharacters?: Array<[CollectionEntry<"users">, string[]]>;
|
||||
lang: Lang;
|
||||
};
|
||||
|
||||
const { copyrightedCharacters, lang } = Astro.props;
|
||||
if (copyrightedCharacters && "" in copyrightedCharacters && Object.keys(copyrightedCharacters).length > 1) {
|
||||
throw new Error("copyrightedCharacter cannot use empty key (catch-all) with other keys");
|
||||
}
|
||||
const charactersPerUser =
|
||||
copyrightedCharacters &&
|
||||
Object.keys(copyrightedCharacters).reduce(
|
||||
(acc, character) => {
|
||||
const key = copyrightedCharacters[character].id;
|
||||
if (!(key in acc)) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(character);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<
|
||||
CollectionEntry<"users">["id"],
|
||||
(typeof copyrightedCharacters extends Record<infer K, any> ? K : never)[]
|
||||
>,
|
||||
);
|
||||
---
|
||||
|
||||
{
|
||||
charactersPerUser ? (
|
||||
copyrightedCharacters ? (
|
||||
<section id="copyrighted-characters">
|
||||
{lang === "eng" ? (
|
||||
<ul>
|
||||
{Object.values(charactersPerUser).map((characterList) => (
|
||||
<li>
|
||||
{characterList[0] === "" ? (
|
||||
<span>
|
||||
All characters are © <UserComponent lang={lang} user={copyrightedCharacters[""]} />
|
||||
</span>
|
||||
) : characterList.length > 2 ? (
|
||||
<span>
|
||||
{characterList.slice(0, characterList.length - 1).join(", ")}, and{" "}
|
||||
{characterList[characterList.length - 1]} are ©{" "}
|
||||
<UserComponent lang={lang} user={copyrightedCharacters[characterList[0]]} />
|
||||
</span>
|
||||
) : characterList.length > 1 ? (
|
||||
<span>
|
||||
{characterList[0]} and {characterList[1]} are ©{" "}
|
||||
<UserComponent lang={lang} user={copyrightedCharacters[characterList[0]]} />
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{characterList[0]} is ©{" "}
|
||||
<UserComponent lang={lang} user={copyrightedCharacters[characterList[0]]} />
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
<ul>
|
||||
{copyrightedCharacters.map(([owner, characterList]) => (
|
||||
<CopyrightedCharactersItem
|
||||
stringFunction={
|
||||
characterList[0] === ""
|
||||
? (user) => t(lang, "characters/all_characters_are_copyrighted_by", user)
|
||||
: (user) => t(lang, "characters/characters_are_copyrighted_by", user, characterList)
|
||||
}
|
||||
>
|
||||
<UserComponent lang={lang} user={owner} />
|
||||
</CopyrightedCharactersItem>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
|
|
10
src/components/CopyrightedCharactersItem.astro
Normal file
10
src/components/CopyrightedCharactersItem.astro
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
type Props = {
|
||||
stringFunction: (_: string) => string;
|
||||
};
|
||||
|
||||
const { stringFunction } = Astro.props;
|
||||
const owner = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
|
||||
---
|
||||
|
||||
{owner ? <li set:html={stringFunction(owner)} /> : null}
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
import { type CollectionEntry } from "astro:content";
|
||||
import { type CollectionEntry, getEntry } from "astro:content";
|
||||
import { t } from "../i18n";
|
||||
import { type Lang } from "../content/config";
|
||||
import { getEntry } from "astro:content";
|
||||
|
||||
type Props = {
|
||||
lang: Lang;
|
||||
|
@ -12,7 +12,7 @@ let { user, lang } = Astro.props;
|
|||
if (user.data.isAnonymous) {
|
||||
user = await getEntry("users", "anonymous");
|
||||
}
|
||||
const username = user.data.nameLang[lang] || user.data.name;
|
||||
const username = t(lang, user.data.nameLang as any) || user.data.name;
|
||||
let link: string | null = null;
|
||||
if (user.data.preferredLink) {
|
||||
if (user.data.preferredLink in user.data.links) {
|
||||
|
|
120
src/i18n/index.ts
Normal file
120
src/i18n/index.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
import { type Lang } from "../content/config";
|
||||
|
||||
export const DEFAULT_LANG = "eng" satisfies Lang;
|
||||
|
||||
export type TranslationRecord = { [DEFAULT_LANG]: string | ((...args: any[]) => string) } & {
|
||||
[L in Exclude<Lang, typeof DEFAULT_LANG>]?: string | ((...args: any[]) => string);
|
||||
};
|
||||
|
||||
export const UI_STRINGS: Record<string, TranslationRecord> = {
|
||||
"story/return_to_stories": {
|
||||
eng: "Return to stories",
|
||||
tok: "o tawa e lipu ale",
|
||||
},
|
||||
"story/return_to_series": {
|
||||
eng: (seriesName: string) => `Return to ${seriesName}`,
|
||||
},
|
||||
"story/go_to_description": {
|
||||
eng: "Go to description",
|
||||
tok: "o tawa e toki lipu",
|
||||
},
|
||||
"story/toggle_dark_mode": {
|
||||
eng: "Toggle dark mode",
|
||||
tok: "o ante e kule lipu",
|
||||
},
|
||||
"story/word_count": {
|
||||
eng: (wordCount: string | number) => `Word count: ${wordCount}.`,
|
||||
tok: "",
|
||||
},
|
||||
"story/publish_date": {
|
||||
eng: (date: string) => date,
|
||||
tok: (date: string) => `tenpo suno ${date}`,
|
||||
},
|
||||
"story/description": {
|
||||
eng: "Description",
|
||||
tok: "toki lipu",
|
||||
},
|
||||
"story/summary": {
|
||||
eng: "Summary",
|
||||
tok: "lipu tawa tenpo lili",
|
||||
},
|
||||
"story/reveal_summary": {
|
||||
eng: "Click to reveal",
|
||||
tok: "Click to reveal summary in English",
|
||||
},
|
||||
"story/to_top": {
|
||||
eng: "To top",
|
||||
tok: "tawa sewi",
|
||||
},
|
||||
"story/tags": {
|
||||
eng: "Tags",
|
||||
tok: "nimi kulupu",
|
||||
},
|
||||
"story/copyright_year": {
|
||||
eng: (year: string | number) => `© ${year}`,
|
||||
tok: (year: string | number) => `© tenpo pi sike suno ${year}`,
|
||||
},
|
||||
"story/licenses": {
|
||||
eng: "Licenses",
|
||||
tok: "lipu lawa",
|
||||
},
|
||||
"story/authors": {
|
||||
eng: (authorsList: string[]) => {
|
||||
let authorsString = `by ${authorsList[0]}`;
|
||||
if (authorsList.length > 2) {
|
||||
authorsString += `, ${authorsList.slice(1, authorsList.length - 1).join(", ")}, and ${authorsList[authorsList.length - 1]}`;
|
||||
} else if (authorsList.length == 2) {
|
||||
authorsString += ` and ${authorsList[1]}`;
|
||||
}
|
||||
return authorsString;
|
||||
},
|
||||
tok: (authorsList: string[]) => {
|
||||
let authorsString = "lipu ni li tan ";
|
||||
if (authorsList.length > 1) {
|
||||
authorsString += `jan ni: ${authorsList.join(" en ")}`;
|
||||
} else {
|
||||
authorsString += authorsList[0];
|
||||
}
|
||||
return authorsString;
|
||||
},
|
||||
},
|
||||
"story/commissioned_by": {
|
||||
eng: (arg: string) => `Commissioned by ${arg}`,
|
||||
},
|
||||
"story/requested_by": {
|
||||
eng: (arg: string) => `Requested by ${arg}`,
|
||||
},
|
||||
"characters/characters_are_copyrighted_by": {
|
||||
eng: (owner: string, charactersList: string[]) => {
|
||||
if (charactersList.length == 1) {
|
||||
return `${charactersList[0]} is © ${owner}`;
|
||||
}
|
||||
if (charactersList.length == 2) {
|
||||
return `${charactersList[0]} and ${charactersList[1]} are © ${owner}`;
|
||||
}
|
||||
return `${charactersList.slice(0, -1).join(", ")}, and ${charactersList[charactersList.length - 1]} are © ${owner}`;
|
||||
},
|
||||
},
|
||||
"characters/all_characters_are_copyrighted_by": {
|
||||
eng: (owner: string) => `All characters are © ${owner}`,
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
|
@ -1,25 +1,59 @@
|
|||
---
|
||||
import { getImage } from "astro:assets";
|
||||
import { type CollectionEntry, getEntry, getEntries } from "astro:content";
|
||||
import { type CollectionEntry, getEntry, getEntries, getCollection } from "astro:content";
|
||||
import { Markdown } from "@astropub/md";
|
||||
import { slug } from "github-slugger";
|
||||
import { DEFAULT_LANG, t } from "../i18n";
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import Authors from "../components/Authors.astro";
|
||||
import CopyrightedCharacters from "../components/CopyrightedCharacters.astro";
|
||||
import Prose from "../components/Prose.astro";
|
||||
import MastodonComments from "../components/MastodonComments.astro";
|
||||
import UserComponent from "../components/UserComponent.astro";
|
||||
|
||||
type Props = CollectionEntry<"games">["data"];
|
||||
|
||||
const { props } = Astro;
|
||||
const series = props.series && (await getEntry(props.series));
|
||||
const authors = await getEntries([props.authors].flat());
|
||||
const copyrightedCharacters: Record<string, CollectionEntry<"users">> = {};
|
||||
Object.keys(props.copyrightedCharacters).forEach(async (character) => {
|
||||
copyrightedCharacters[character] = await getEntry(props.copyrightedCharacters[character]);
|
||||
});
|
||||
if ("" in props.copyrightedCharacters && Object.keys(props.copyrightedCharacters).length > 1) {
|
||||
throw new Error("copyrightedCharacter cannot use empty key (catch-all) with other keys");
|
||||
}
|
||||
const copyrightedCharacters = await Promise.all(
|
||||
Object.values(
|
||||
Object.keys(props.copyrightedCharacters).reduce(
|
||||
(acc, character) => {
|
||||
const user = props.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 relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||
// 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((tag) => {
|
||||
if (typeof tag === "string") {
|
||||
return [tag, tag];
|
||||
}
|
||||
return [t(DEFAULT_LANG, tag as any), t(props.lang, tag as any)];
|
||||
}),
|
||||
),
|
||||
);
|
||||
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 tag-categories!`);
|
||||
return [tagSlug, tag];
|
||||
}
|
||||
return [tagSlug, categorizedTags[tag]!];
|
||||
});
|
||||
const thumbnail =
|
||||
props.thumbnail &&
|
||||
(await getImage({ src: props.thumbnail, width: props.thumbnailWidth, height: props.thumbnailHeight }));
|
||||
|
@ -37,8 +71,14 @@ const thumbnail =
|
|||
id="top"
|
||||
class="min-w-screen relative min-h-screen bg-radial from-bm-300 to-bm-600 px-1 pb-16 pt-20 dark:from-green-700 dark:to-green-950 print:bg-none"
|
||||
>
|
||||
<div id="toolbox-buttons" aria-label="Toolbox" class="absolute top-0 h-[80vh] py-2 pl-2 lg:inset-y-0 lg:h-full">
|
||||
<div class="sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden">
|
||||
<div
|
||||
id="toolbox-buttons"
|
||||
aria-label="Toolbox"
|
||||
class="pointer-events-none absolute top-0 h-[80vh] py-2 pl-2 lg:inset-y-0 lg:h-full"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-auto sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden"
|
||||
>
|
||||
<a
|
||||
href={series ? series.data.url : "/games"}
|
||||
class="text-link my-1 h-9 w-9 p-2"
|
||||
|
@ -89,7 +129,9 @@ const thumbnail =
|
|||
id="game-information"
|
||||
class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200"
|
||||
>
|
||||
<Authors authors={authors} lang={props.lang} />
|
||||
<Authors lang={props.lang}>
|
||||
{authors.map((author) => <UserComponent lang={props.lang} user={author} />)}
|
||||
</Authors>
|
||||
{
|
||||
props.isDraft ? (
|
||||
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
||||
|
@ -142,9 +184,7 @@ const thumbnail =
|
|||
year: "numeric",
|
||||
})}
|
||||
>
|
||||
{props.lang === "tok"
|
||||
? `tenpo suno ${props.pubDate.toISOString().slice(undefined, 10)}`
|
||||
: props.pubDate.toISOString().slice(undefined, 10)}
|
||||
{t(props.lang, "story/publish_date", props.pubDate.toISOString().slice(undefined, 10))}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
@ -170,10 +210,10 @@ const thumbnail =
|
|||
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">Tags</h2>
|
||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{
|
||||
props.tags.map((tag) => (
|
||||
tags.map(([tagSlug, tagText]) => (
|
||||
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white print:bg-none">
|
||||
<a class="hover:underline focus:underline" href={`/tags/${slug(tag)}`}>
|
||||
{tag}
|
||||
<a class="hover:underline focus:underline" href={`/tags/${tagSlug}`}>
|
||||
{tagText}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
|
@ -187,13 +227,9 @@ const thumbnail =
|
|||
/>
|
||||
</main>
|
||||
<div class="pt-6 text-center text-xs text-black dark:text-white">
|
||||
<span
|
||||
>© {
|
||||
props.lang === "tok" ? `tenpo pi sike suno ${props.pubDate.getFullYear()}` : props.pubDate.getFullYear()
|
||||
} |
|
||||
</span>
|
||||
<span>{t(props.lang, "story/copyright_year", props.pubDate.getFullYear())} | </span>
|
||||
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
||||
>{props.lang === "eng" ? "Licenses" : props.lang === "tok" ? "lipu lawa" : null}</a
|
||||
>{t(props.lang, "story/licenses")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { getImage } from "astro:assets";
|
|||
import { type CollectionEntry, getEntry, getEntries, getCollection } from "astro:content";
|
||||
import { Markdown } from "@astropub/md";
|
||||
import { slug } from "github-slugger";
|
||||
import { DEFAULT_LANG, t } from "../i18n";
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import Authors from "../components/Authors.astro";
|
||||
import UserComponent from "../components/UserComponent.astro";
|
||||
|
@ -25,10 +26,24 @@ const series = props.series && (await getEntry(props.series));
|
|||
const authors = await getEntries([props.authors].flat());
|
||||
const commissioner = props.commissioner && (await getEntry(props.commissioner));
|
||||
const requester = props.requester && (await getEntry(props.requester));
|
||||
const copyrightedCharacters: Record<string, CollectionEntry<"users">> = {};
|
||||
Object.keys(props.copyrightedCharacters).forEach(async (character) => {
|
||||
copyrightedCharacters[character] = await getEntry(props.copyrightedCharacters[character]);
|
||||
});
|
||||
if ("" in props.copyrightedCharacters && Object.keys(props.copyrightedCharacters).length > 1) {
|
||||
throw new Error("copyrightedCharacter cannot use empty key (catch-all) with other keys");
|
||||
}
|
||||
const copyrightedCharacters = await Promise.all(
|
||||
Object.values(
|
||||
Object.keys(props.copyrightedCharacters).reduce(
|
||||
(acc, character) => {
|
||||
const user = props.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 relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||
// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||
const categorizedTags = Object.fromEntries(
|
||||
|
@ -37,11 +52,7 @@ const categorizedTags = Object.fromEntries(
|
|||
if (typeof tag === "string") {
|
||||
return [tag, tag];
|
||||
}
|
||||
const key = tag["eng"]!;
|
||||
if (props.lang in tag) {
|
||||
return [key, tag[props.lang]!];
|
||||
}
|
||||
return [key, key];
|
||||
return [t(DEFAULT_LANG, tag as any), t(props.lang, tag as any)];
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
@ -70,16 +81,20 @@ const thumbnail =
|
|||
id="top"
|
||||
class="min-w-screen relative min-h-screen bg-radial from-bm-300 to-bm-600 px-1 pb-16 pt-20 dark:from-green-700 dark:to-green-950 print:bg-none"
|
||||
>
|
||||
<div id="toolbox-buttons" aria-label="Toolbox" class="absolute top-0 h-[80vh] py-2 pl-2 lg:inset-y-0 lg:h-full">
|
||||
<div class="sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden">
|
||||
<div
|
||||
id="toolbox-buttons"
|
||||
aria-label="Toolbox"
|
||||
class="pointer-events-none absolute top-0 h-[80vh] py-2 pl-2 lg:inset-y-0 lg:h-full"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-auto sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden"
|
||||
>
|
||||
<a
|
||||
href={series ? series.data.url : "/stories/1"}
|
||||
class="text-link my-1 h-9 w-9 p-2"
|
||||
aria-label={props.lang === "eng"
|
||||
? `Return to ${series ? series.data.name : "stories"}`
|
||||
: props.lang === "tok"
|
||||
? "o tawa e lipu ale"
|
||||
: null}
|
||||
aria-label={series
|
||||
? t(props.lang, "story/return_to_series", series.data.name)
|
||||
: t(props.lang, "story/return_to_stories")}
|
||||
>
|
||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||
<path
|
||||
|
@ -90,7 +105,7 @@ const thumbnail =
|
|||
<a
|
||||
href="#description"
|
||||
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-label={props.lang === "eng" ? "Go to description" : props.lang === "tok" ? "o tawa e toki lipu" : null}
|
||||
aria-label={t(props.lang, "story/go_to_description")}
|
||||
>
|
||||
<svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
|
||||
<path
|
||||
|
@ -101,7 +116,7 @@ const thumbnail =
|
|||
<button
|
||||
data-dark-mode
|
||||
class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-label={props.lang === "eng" ? "Toggle dark mode" : props.lang === "tok" ? "o ante e kule" : null}
|
||||
aria-label={t(props.lang, "story/toggle_dark_mode")}
|
||||
>
|
||||
<svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
|
||||
<path
|
||||
|
@ -161,7 +176,9 @@ const thumbnail =
|
|||
id="story-information"
|
||||
class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200"
|
||||
>
|
||||
<Authors authors={authors} lang={props.lang} />
|
||||
<Authors lang={props.lang}>
|
||||
{authors.map((author) => <UserComponent lang={props.lang} user={author} />)}
|
||||
</Authors>
|
||||
{
|
||||
props.isDraft ? (
|
||||
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
|
||||
|
@ -185,7 +202,7 @@ const thumbnail =
|
|||
}
|
||||
<div id="content-warning">
|
||||
<p>
|
||||
{props.lang === "eng" ? `Word count: ${props.wordCount}.` : props.lang === "tok" ? `` : null}
|
||||
{t(props.lang, "story/word_count", props.wordCount)}
|
||||
{props.contentWarning}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -231,15 +248,13 @@ const thumbnail =
|
|||
year: "numeric",
|
||||
})}
|
||||
>
|
||||
{props.lang === "tok"
|
||||
? `tenpo suno ${props.pubDate.toISOString().slice(undefined, 10)}`
|
||||
: props.pubDate.toISOString().slice(undefined, 10)}
|
||||
{t(props.lang, "story/publish_date", props.pubDate.toISOString().slice(undefined, 10))}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
||||
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{props.lang === "eng" ? "Description" : props.lang === "tok" ? "toki lipu" : null}
|
||||
{t(props.lang, "story/description")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
|
@ -250,15 +265,11 @@ const thumbnail =
|
|||
props.summary ? (
|
||||
<section id="summary" class="px-2 font-serif" aria-describedby="title-summary">
|
||||
<h2 id="title-summary" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{props.lang === "eng" ? "Summary" : props.lang === "tok" ? "lipu tawa tenpo lili" : null}
|
||||
{t(props.lang, "story/summary")}
|
||||
</h2>
|
||||
<details class="mb-6 mt-1 rounded-lg border border-stone-400 bg-stone-50 text-stone-800 dark:border-stone-500 dark:bg-stone-900 dark:text-stone-100">
|
||||
<summary class="rounded-lg bg-stone-200 px-2 py-1 dark:bg-stone-800">
|
||||
{props.lang === "eng"
|
||||
? "Click to reveal"
|
||||
: props.lang === "tok"
|
||||
? "Click to reveal summary in English"
|
||||
: null}
|
||||
{t(props.lang, "story/reveal_summary")}
|
||||
</summary>
|
||||
<div class="px-2 py-1">
|
||||
<Prose>
|
||||
|
@ -275,7 +286,7 @@ const thumbnail =
|
|||
><path
|
||||
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
|
||||
></path></svg
|
||||
><span>{props.lang === "eng" ? "To top" : props.lang === "tok" ? "tawa sewi" : null}</span></a
|
||||
><span>{t(props.lang, "story/to_top")}</span></a
|
||||
>
|
||||
</div>
|
||||
{
|
||||
|
@ -334,7 +345,7 @@ const thumbnail =
|
|||
}
|
||||
<section id="tags" aria-describedby="title-tags" class="my-5">
|
||||
<h2 id="title-tags" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{props.lang === "eng" ? "Tags" : props.lang === "tok" ? "nimi kulupu" : null}
|
||||
{t(props.lang, "story/tags")}
|
||||
</h2>
|
||||
<ul class="flex flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
{
|
||||
|
@ -355,13 +366,9 @@ const thumbnail =
|
|||
/>
|
||||
</main>
|
||||
<div class="pt-6 text-center text-xs text-black dark:text-white">
|
||||
<span
|
||||
>© {
|
||||
props.lang === "tok" ? `tenpo pi sike suno ${props.pubDate.getFullYear()}` : props.pubDate.getFullYear()
|
||||
} |
|
||||
</span>
|
||||
<span>{t(props.lang, "story/copyright_year", props.pubDate.getFullYear())} | </span>
|
||||
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
|
||||
>{props.lang === "eng" ? "Licenses" : props.lang === "tok" ? "lipu lawa" : null}</a
|
||||
>{t(props.lang, "story/licenses")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro
|
|||
import { marked, type RendererApi } from "marked";
|
||||
import { decode as tinyDecode } from "tiny-decode";
|
||||
import { type Lang, type Website } from "../../../content/config";
|
||||
import { t } from "../../../i18n";
|
||||
|
||||
type DescriptionFormat = "bbcode" | "markdown";
|
||||
|
||||
|
@ -196,9 +197,9 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsite):
|
|||
|
||||
function getNameForUser(user: CollectionEntry<"users">, anonymousUser: CollectionEntry<"users">, lang: Lang): string {
|
||||
if (user.data.isAnonymous) {
|
||||
return anonymousUser.data.nameLang[lang] || anonymousUser.data.name;
|
||||
return t(lang, anonymousUser.data.nameLang as any) || anonymousUser.data.name;
|
||||
}
|
||||
return user.data.nameLang[lang] || user.data.name;
|
||||
return t(lang, user.data.nameLang as any) || user.data.name;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
|
@ -253,7 +254,7 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
|||
[
|
||||
story.data.description,
|
||||
`*Word count: ${story.data.wordCount}. ${story.data.contentWarning.trim()}*`,
|
||||
"Writing: " + (await getEntries([story.data.authors].flat())).map((author) => u(author)).join(" , "),
|
||||
"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(
|
||||
|
@ -301,35 +302,11 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
|||
const commissioner = story.data.commissioner && (await getEntry(story.data.commissioner));
|
||||
const requester = story.data.requester && (await getEntry(story.data.requester));
|
||||
|
||||
let storyHeader = `${story.data.title}\n`;
|
||||
if (lang === "eng") {
|
||||
let authorsString = `by ${authorsNames[0]}`;
|
||||
if (authorsNames.length > 2) {
|
||||
authorsString += `, ${authorsNames.slice(1, authorsNames.length - 1).join(", ")}, and ${authorsNames[authorsNames.length - 1]}`;
|
||||
} else if (authorsNames.length == 2) {
|
||||
authorsString += ` and ${authorsNames[1]}`;
|
||||
}
|
||||
storyHeader +=
|
||||
`${authorsString}\n` +
|
||||
(commissioner ? `Commissioned by ${getNameForUser(commissioner, anonymousUser, lang)}\n` : "") +
|
||||
(requester ? `Requested by ${getNameForUser(requester, anonymousUser, lang)}\n` : "");
|
||||
} else if (lang === "tok") {
|
||||
let authorsString = "lipu ni li tan ";
|
||||
if (authorsNames.length > 1) {
|
||||
authorsString += `jan ni: ${authorsNames.join(" en ")}`;
|
||||
} else {
|
||||
authorsString += authorsNames[0];
|
||||
}
|
||||
if (commissioner) {
|
||||
throw new Error(`No "commissioner" handler for language "tok"`);
|
||||
}
|
||||
if (requester) {
|
||||
throw new Error(`No "requester" handler for language "tok"`);
|
||||
}
|
||||
storyHeader += `${authorsString}\n`;
|
||||
} else {
|
||||
throw new Error(`Unknown language "${lang}"`);
|
||||
}
|
||||
const storyHeader =
|
||||
`${story.data.title}\n` +
|
||||
`${t(lang, "story/authors", authorsNames)}\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("\\=", "=")}`
|
||||
.replaceAll(/\n\n\n+/g, "\n\n")
|
||||
|
|
|
@ -53,7 +53,7 @@ const categorizedTags: Array<[string, string, string[]]> = tagCategories
|
|||
return tag;
|
||||
}
|
||||
if (!("eng" in tag)) {
|
||||
throw new Error(`"{[lang]: text}" tag must have an "eng" key: ${tag}`);
|
||||
throw new Error(`Object-formatted tag must have an "eng" key: ${tag}`);
|
||||
}
|
||||
return tag["eng"]!;
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue