- Improved schema validation - Move username parsing and other validators to schema types - Fix astro check command - Add JSON/YAML schema validation for data collections - Update licenses - Remove deployment script in favor of rsync - Prevent unsanitized input in export-story script - Change "eng" language to "en", per BCP47 - Clean up i18n keys and add aria attributes - Improve MastodonComments behavior on no-JS browsers
249 lines
8.2 KiB
TypeScript
249 lines
8.2 KiB
TypeScript
import type { GamePlatform, Lang } from "../content/config";
|
||
import { DEFAULT_LANG } from "../content/config";
|
||
export { DEFAULT_LANG } from "../content/config";
|
||
|
||
const UI_STRINGS = {
|
||
// Utility functions
|
||
"util/join_names": {
|
||
en: (names: string[]) =>
|
||
names.length <= 1
|
||
? names.join("")
|
||
: names.length == 2
|
||
? `${names[0]} and ${names[1]}`
|
||
: `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`,
|
||
tok: (names: string[]) => names.join(" en "),
|
||
},
|
||
"util/capitalize": {
|
||
en: (text: string) => (text.length > 0 ? `${text[0].toUpperCase()}${text.slice(1)}` : ""),
|
||
},
|
||
"util/enumerate": {
|
||
en: (count: number, nounSingular: string, nounPlural?: string) => {
|
||
if (count == 0) {
|
||
return `no ${nounPlural ?? nounSingular}`;
|
||
}
|
||
if (count == 1) {
|
||
return `one ${nounSingular}`;
|
||
}
|
||
return `${count} ${nounPlural ?? nounSingular}`;
|
||
},
|
||
tok: (count: number, nounSingular: string, nounPlural?: string) =>
|
||
`${(count > 1 && nounPlural) || nounSingular} ${["ala", "wan", "tu"][count] || "mute"}`,
|
||
},
|
||
// export-story API functions
|
||
"export_story/authors": {
|
||
en: (authorsList: string[]) => `Writing: ${authorsList.join(" ")}`,
|
||
tok: (authorsList: string[]) => `lipu ni li tan jan ni: ${authorsList.join(" en ")}`,
|
||
},
|
||
"export_story/request_for": {
|
||
en: (requesterList: string[]) => `Request for: ${requesterList.join(" ")}`,
|
||
},
|
||
"export_story/commissioned_by": {
|
||
en: (commissionerList: string[]) => `Commissioned by: ${commissionerList.join(" ")}`,
|
||
},
|
||
// Shared strings for published content (stories + games)
|
||
"published_content/return_to_series": {
|
||
en: (seriesName: string) => `Return to ${seriesName}`,
|
||
},
|
||
"published_content/go_to_description": {
|
||
en: "Go to description",
|
||
tok: "o tawa e toki lipu",
|
||
},
|
||
"published_content/toggle_dark_mode": {
|
||
en: "Toggle dark mode",
|
||
tok: "o ante e kule lipu",
|
||
},
|
||
"published_content/cover_art_alt": {
|
||
en: (title: string) => `Cover art for ${title}`,
|
||
tok: (_title: string) => `sitelen tawa lipu ni`,
|
||
},
|
||
"published_content/publish_date": {
|
||
en: (date: Date) => date.toISOString().slice(0, 10),
|
||
tok: (date: Date) => `tenpo suno ${date.toISOString().slice(0, 10)}`,
|
||
},
|
||
"published_content/publish_date_aria_label": {
|
||
en: "Publish date",
|
||
tok: "tenpo pi pana lipu",
|
||
},
|
||
"published_content/publish_date_aria_description": {
|
||
en: (date: Date) =>
|
||
date.toLocaleDateString("en-US", {
|
||
month: "long",
|
||
day: "numeric",
|
||
year: "numeric",
|
||
}),
|
||
tok: (_date: Date) => "",
|
||
},
|
||
"published_content/description": {
|
||
en: "Description",
|
||
tok: "toki lipu",
|
||
},
|
||
"published_content/to_top": {
|
||
en: "To top",
|
||
tok: "tawa sewi",
|
||
},
|
||
"published_content/tags": {
|
||
en: "Tags",
|
||
tok: "nimi kulupu",
|
||
},
|
||
"published_content/copyright_year": {
|
||
en: (year: string | number) => `© ${year}`,
|
||
tok: (year: string | number) => `© tenpo pi sike suno ${year}`,
|
||
},
|
||
"published_content/licenses": {
|
||
en: "Licenses",
|
||
tok: "lipu lawa",
|
||
},
|
||
"published_content/draft_warning": {
|
||
en: "DRAFT VERSION – DO NOT REDISTRIBUTE",
|
||
},
|
||
"published_content/related_stories": {
|
||
en: "Related stories",
|
||
},
|
||
"published_content/related_games": {
|
||
en: "Related games",
|
||
},
|
||
// Story page-specific strings
|
||
"story/return_to_stories": {
|
||
en: "Return to stories",
|
||
tok: "o tawa e lipu ale",
|
||
},
|
||
"story/title_aria_label": {
|
||
en: "Story title",
|
||
tok: "nimi lipu",
|
||
},
|
||
"story/authors_aria_label": {
|
||
en: (authors: any[]) => (authors.length == 1 ? "Author" : "Authors"),
|
||
tok: (_authors: any[]) => "jan pi pali lipu",
|
||
},
|
||
"story/requesters_aria_label": {
|
||
en: (requesters: any[]) => (requesters.length == 1 ? "Requester" : "Requesters"),
|
||
},
|
||
"story/commissioners_aria_label": {
|
||
en: (commissioners: any[]) => (commissioners.length == 1 ? "Commissioner" : "Commissioners"),
|
||
},
|
||
"story/warnings": {
|
||
en: (wordCount: number | string | undefined, contentWarning: string) =>
|
||
wordCount ? `Word count: ${wordCount}. ${contentWarning}` : contentWarning,
|
||
tok: (_wordCount: number | string | undefined, contentWarning: string) => contentWarning,
|
||
},
|
||
"story/article_aria_label": {
|
||
en: "Story",
|
||
tok: "lipu",
|
||
},
|
||
"story/summary": {
|
||
en: "Summary",
|
||
tok: "lipu tawa tenpo lili",
|
||
},
|
||
"story/reveal_summary": {
|
||
en: "Click to reveal",
|
||
tok: "Click to reveal summary in English",
|
||
},
|
||
"story/authors": {
|
||
en: (authorsList: string[]) => `by ${UI_STRINGS["util/join_names"].en(authorsList)}`,
|
||
tok: (authorsList: string[]) =>
|
||
authorsList.length > 1
|
||
? `lipu ni li tan jan ni: ${UI_STRINGS["util/join_names"].tok(authorsList)}`
|
||
: `lipu ni li tan ${authorsList[0]}`,
|
||
},
|
||
"story/commissioned_by": {
|
||
en: (commissionersList: string[]) => `Commissioned by ${UI_STRINGS["util/join_names"].en(commissionersList)}`,
|
||
},
|
||
"story/requested_by": {
|
||
en: (requestersList: string[]) => `Requested by ${UI_STRINGS["util/join_names"].en(requestersList)}`,
|
||
},
|
||
// Game page-specific strings
|
||
"game/return_to_games": {
|
||
en: "Return to games",
|
||
},
|
||
"game/title_aria_label": {
|
||
en: "Game title",
|
||
},
|
||
"game/platforms": {
|
||
en: (platforms: GamePlatform[]) => {
|
||
if (platforms.length == 0) {
|
||
return "";
|
||
}
|
||
const translatedPlatforms = platforms.map((platform) => {
|
||
const platformLang = UI_STRINGS[`game/platform_${platform}`].en;
|
||
if (!platformLang) {
|
||
throw new Error(`Invalid platform "${platform}"`);
|
||
}
|
||
return platformLang;
|
||
});
|
||
return `A game for ${UI_STRINGS["util/join_names"].en(translatedPlatforms)}.`;
|
||
},
|
||
},
|
||
"game/platform_web": {
|
||
en: "web browsers",
|
||
},
|
||
"game/platform_windows": {
|
||
en: "Windows",
|
||
},
|
||
"game/platform_linux": {
|
||
en: "Linux",
|
||
},
|
||
"game/platform_macos": {
|
||
en: "macOS",
|
||
},
|
||
"game/platform_android": {
|
||
en: "Android",
|
||
},
|
||
"game/platform_ios": {
|
||
en: "iOS",
|
||
},
|
||
"game/warnings": {
|
||
en: (platforms: GamePlatform[], contentWarning: string) =>
|
||
platforms.length > 0 ? `${UI_STRINGS["game/platforms"].en(platforms)} ${contentWarning}` : contentWarning,
|
||
},
|
||
"game/article_aria_label": {
|
||
en: "Game",
|
||
},
|
||
// Copyrighted character-related strings
|
||
"characters/copyrighted_characters_aria_label": {
|
||
en: "Copyrighted characters",
|
||
},
|
||
"characters/characters_are_copyrighted_by": {
|
||
en: (owner: string, charactersList: string[]) =>
|
||
charactersList.length == 1
|
||
? `${charactersList[0]} is © ${owner}`
|
||
: `${UI_STRINGS["util/join_names"].en(charactersList)} are © ${owner}`,
|
||
},
|
||
"characters/all_characters_are_copyrighted_by": {
|
||
en: (owner: string) => `All characters are © ${owner}`,
|
||
},
|
||
// Tag-related strings
|
||
"tag/total_works_with_tag": {
|
||
en: (tag: string, storiesCount: number, gamesCount: number) => {
|
||
const content = [];
|
||
if (storiesCount > 0) {
|
||
content.push(UI_STRINGS["util/enumerate"].en(storiesCount, "story", "stories"));
|
||
}
|
||
if (gamesCount > 0) {
|
||
content.push(UI_STRINGS["util/enumerate"].en(gamesCount, "game", "games"));
|
||
}
|
||
if (content.length == 0) {
|
||
return `No works tagged with "${tag}".`;
|
||
}
|
||
return UI_STRINGS["util/capitalize"].en(`${UI_STRINGS["util/join_names"].en(content)} tagged with "${tag}".`);
|
||
},
|
||
},
|
||
} as const;
|
||
|
||
type TranslationKey = keyof typeof UI_STRINGS;
|
||
type TranslationEntry<T> = { [DEFAULT_LANG]: T } & {
|
||
[L in Exclude<Lang, typeof DEFAULT_LANG>]?: T;
|
||
};
|
||
type TranslationArgs<K extends TranslationKey> =
|
||
(typeof UI_STRINGS)[K] extends TranslationEntry<infer T> ? (T extends (...args: infer A) => string ? A : []) : never;
|
||
|
||
/** Translates some text according to the provided language, a translation key,
|
||
* and optionally any required arguments.
|
||
*/
|
||
export function t<K extends TranslationKey>(lang: Lang, key: K, ...args: TranslationArgs<K>): string {
|
||
if (key in UI_STRINGS) {
|
||
const translation: string | ((...args: TranslationArgs<K>) => string) =
|
||
(UI_STRINGS[key] as any)[lang] || UI_STRINGS[key][DEFAULT_LANG];
|
||
return typeof translation === "function" ? translation(...args) : translation;
|
||
}
|
||
throw new Error(`No translation entry found for key "${key}"`);
|
||
}
|