From 4f83ae8802f8a7b58930714e9d54d995dda930f1 Mon Sep 17 00:00:00 2001 From: Bad Manners <me@badmanners.xyz> Date: Fri, 29 Mar 2024 22:48:36 -0300 Subject: [PATCH] Add i18n module and fix missing CopyrightedCharacters --- package-lock.json | 4 +- package.json | 2 +- src/components/Authors.astro | 55 +------- src/components/CopyrightedCharacters.astro | 67 +++------- .../CopyrightedCharactersItem.astro | 10 ++ src/components/UserComponent.astro | 6 +- src/i18n/index.ts | 120 ++++++++++++++++++ src/layouts/GameLayout.astro | 76 ++++++++--- src/layouts/StoryLayout.astro | 83 ++++++------ src/pages/api/export-story/[...slug].ts | 41 ++---- src/pages/tags.astro | 2 +- 11 files changed, 270 insertions(+), 196 deletions(-) create mode 100644 src/components/CopyrightedCharactersItem.astro create mode 100644 src/i18n/index.ts diff --git a/package-lock.json b/package-lock.json index 4b00508..78fd541 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c4508a2..37626ab 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Authors.astro b/src/components/Authors.astro index 779efbe..0e8347c 100644 --- a/src/components/Authors.astro +++ b/src/components/Authors.astro @@ -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} diff --git a/src/components/CopyrightedCharacters.astro b/src/components/CopyrightedCharacters.astro index a1fd2db..e4f2936 100644 --- a/src/components/CopyrightedCharacters.astro +++ b/src/components/CopyrightedCharacters.astro @@ -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 } diff --git a/src/components/CopyrightedCharactersItem.astro b/src/components/CopyrightedCharactersItem.astro new file mode 100644 index 0000000..b18a8a1 --- /dev/null +++ b/src/components/CopyrightedCharactersItem.astro @@ -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} diff --git a/src/components/UserComponent.astro b/src/components/UserComponent.astro index 767f46b..b07eb85 100644 --- a/src/components/UserComponent.astro +++ b/src/components/UserComponent.astro @@ -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) { diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..2c64c6c --- /dev/null +++ b/src/i18n/index.ts @@ -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; +} diff --git a/src/layouts/GameLayout.astro b/src/layouts/GameLayout.astro index 96b5b2e..5f17b12 100644 --- a/src/layouts/GameLayout.astro +++ b/src/layouts/GameLayout.astro @@ -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> diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro index 57cbdd0..60ba7cc 100644 --- a/src/layouts/StoryLayout.astro +++ b/src/layouts/StoryLayout.astro @@ -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> diff --git a/src/pages/api/export-story/[...slug].ts b/src/pages/api/export-story/[...slug].ts index 5bea164..f769719 100644 --- a/src/pages/api/export-story/[...slug].ts +++ b/src/pages/api/export-story/[...slug].ts @@ -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") diff --git a/src/pages/tags.astro b/src/pages/tags.astro index 4d2a280..0c10217 100644 --- a/src/pages/tags.astro +++ b/src/pages/tags.astro @@ -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"]!; });