Add i18n module and fix missing CopyrightedCharacters

This commit is contained in:
Bad Manners 2024-03-29 22:48:36 -03:00
parent 7ca6f52cc2
commit 4f83ae8802
11 changed files with 270 additions and 196 deletions

View file

@ -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
>&copy; {
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>

View file

@ -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
>&copy; {
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>