Wordings now comes from CMS
This commit is contained in:
parent
f4cede5240
commit
f76ac860e5
|
@ -11,7 +11,8 @@
|
||||||
"upgrade": "ncu",
|
"upgrade": "ncu",
|
||||||
"script:download-payload-sdk": "bun run scripts/download-payload-sdk.ts",
|
"script:download-payload-sdk": "bun run scripts/download-payload-sdk.ts",
|
||||||
"script:download-currencies": "bun run scripts/download-currencies.ts",
|
"script:download-currencies": "bun run scripts/download-currencies.ts",
|
||||||
"script:download-wording-keys": "bun run scripts/download-wording-keys.ts"
|
"script:download-wording-keys": "bun run scripts/download-wording-keys.ts",
|
||||||
|
"precommit": "bun run script:download-wording-keys && bun run script:download-payload-sdk && bun run astro check"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">=10.0.0",
|
"npm": ">=10.0.0",
|
||||||
|
@ -27,8 +28,7 @@
|
||||||
"astro-icon": "^1.1.0",
|
"astro-icon": "^1.1.0",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"ua-parser-js": "^1.0.37",
|
"ua-parser-js": "^1.0.37"
|
||||||
"zod": "^3.22.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/material-symbols": "^1.1.73",
|
"@iconify-json/material-symbols": "^1.1.73",
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { writeFileSync } from "fs";
|
||||||
|
import { payload } from "src/shared/payload/payload-sdk";
|
||||||
|
|
||||||
|
const TRANSLATION_FOLDER = `${process.cwd()}/src/i18n`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wordings = await payload.getWordings();
|
||||||
|
const keys = wordings.map(({ name }) => name);
|
||||||
|
|
||||||
|
let result = "";
|
||||||
|
result += "export type WordingKey =\n";
|
||||||
|
result += ` | "` + keys.join(`"\n | "`) + `";\n`;
|
||||||
|
|
||||||
|
writeFileSync(`${TRANSLATION_FOLDER}/wordings-keys.ts`, result, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to get the sdk", e);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import { getI18n } from "translations/translations";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
withLinks: boolean;
|
withLinks: boolean;
|
||||||
|
|
|
@ -4,9 +4,9 @@ import Button from "components/Button.astro";
|
||||||
import ThemeSelector from "./components/ThemeSelector.astro";
|
import ThemeSelector from "./components/ThemeSelector.astro";
|
||||||
import LanguageSelector from "./components/LanguageSelector.astro";
|
import LanguageSelector from "./components/LanguageSelector.astro";
|
||||||
import CurrencySelector from "./components/CurrencySelector.astro";
|
import CurrencySelector from "./components/CurrencySelector.astro";
|
||||||
import { getI18n } from "translations/translations";
|
|
||||||
import type { ParentPage } from "src/shared/payload/payload-sdk";
|
import type { ParentPage } from "src/shared/payload/payload-sdk";
|
||||||
import ParentPagesButton from "./components/ParentPagesButton.astro";
|
import ParentPagesButton from "./components/ParentPagesButton.astro";
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parentPages?: ParentPage[] | undefined;
|
parentPages?: ParentPage[] | undefined;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
import Button from "components/Button.astro";
|
import Button from "components/Button.astro";
|
||||||
import Tooltip from "components/Tooltip.astro";
|
import Tooltip from "components/Tooltip.astro";
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import { cache } from "src/utils/cachedPayload";
|
import { cache } from "src/utils/cachedPayload";
|
||||||
import { getI18n } from "translations/translations";
|
|
||||||
import { formatCurrency } from "src/utils/currencies";
|
import { formatCurrency } from "src/utils/currencies";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
---
|
---
|
||||||
import Button from "components/Button.astro";
|
import Button from "components/Button.astro";
|
||||||
import Tooltip from "components/Tooltip.astro";
|
import Tooltip from "components/Tooltip.astro";
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import { cache } from "src/utils/cachedPayload";
|
import { cache } from "src/utils/cachedPayload";
|
||||||
import { getI18n } from "translations/translations";
|
import { formatLocale } from "src/utils/format";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
withTitle?: boolean | undefined;
|
withTitle?: boolean | undefined;
|
||||||
|
@ -12,7 +13,7 @@ interface Props {
|
||||||
const { withTitle, class: className } = Astro.props;
|
const { withTitle, class: className } = Astro.props;
|
||||||
|
|
||||||
const { currentLocale } = Astro.locals;
|
const { currentLocale } = Astro.locals;
|
||||||
const { t, formatLocale } = await getI18n(currentLocale);
|
const { t } = await getI18n(currentLocale);
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import { Collections, type ParentPage } from "src/shared/payload/payload-sdk";
|
import { Collections, type ParentPage } from "src/shared/payload/payload-sdk";
|
||||||
import { getI18n } from "translations/translations";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parentPage: ParentPage;
|
parentPage: ParentPage;
|
||||||
|
@ -11,9 +11,7 @@ const { getLocalizedMatch, getLocalizedUrl } = await getI18n(
|
||||||
Astro.locals.currentLocale
|
Astro.locals.currentLocale
|
||||||
);
|
);
|
||||||
|
|
||||||
const translation = getLocalizedMatch(parentPage.translations, {
|
const translation = getLocalizedMatch(parentPage.translations);
|
||||||
name: parentPage.slug,
|
|
||||||
});
|
|
||||||
|
|
||||||
let href = "";
|
let href = "";
|
||||||
switch (parentPage.collection) {
|
switch (parentPage.collection) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Tooltip from "components/Tooltip.astro";
|
||||||
import type { ParentPage } from "src/shared/payload/payload-sdk";
|
import type { ParentPage } from "src/shared/payload/payload-sdk";
|
||||||
import ParentPageLink from "./ParentPageLink.astro";
|
import ParentPageLink from "./ParentPageLink.astro";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import { getI18n } from "translations/translations";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parentPages: ParentPage[];
|
parentPages: ParentPage[];
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import Button from "components/Button.astro";
|
import Button from "components/Button.astro";
|
||||||
import Tooltip from "components/Tooltip.astro";
|
import Tooltip from "components/Tooltip.astro";
|
||||||
import { getI18n } from "translations/translations";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
const { currentLocale, currentTheme } = Astro.locals;
|
const { currentLocale, currentTheme } = Astro.locals;
|
||||||
const { t } = await getI18n(currentLocale);
|
const { t } = await getI18n(currentLocale);
|
||||||
|
@ -15,15 +15,15 @@ const { t } = await getI18n(currentLocale);
|
||||||
<div id="content" slot="tooltip-content">
|
<div id="content" slot="tooltip-content">
|
||||||
<a
|
<a
|
||||||
class:list={{ current: currentTheme === "dark" }}
|
class:list={{ current: currentTheme === "dark" }}
|
||||||
href="?action-theme=dark">{t("header.topbar.theme.dark")}</a
|
href="?action-theme=dark">{t("global.theme.dark")}</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class:list={{ current: currentTheme === "auto" }}
|
class:list={{ current: currentTheme === "auto" }}
|
||||||
href="?action-theme=auto">{t("header.topbar.theme.auto")}</a
|
href="?action-theme=auto">{t("global.theme.auto")}</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class:list={{ current: currentTheme === "light" }}
|
class:list={{ current: currentTheme === "light" }}
|
||||||
href="?action-theme=light">{t("header.topbar.theme.light")}</a
|
href="?action-theme=light">{t("global.theme.light")}</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import { getI18n } from "translations/translations";
|
import { formatRecorder } from "src/utils/format";
|
||||||
import Metadata from "./Metadata.astro";
|
import Metadata from "./Metadata.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -9,7 +9,6 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { translators = [], transcribers = [], proofreaders = [] } = Astro.props;
|
const { translators = [], transcribers = [], proofreaders = [] } = Astro.props;
|
||||||
const { formatRecorder } = await getI18n(Astro.locals.currentLocale);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import MasoActor from "components/Maso/MasoActor.astro";
|
import MasoActor from "components/Maso/MasoActor.astro";
|
||||||
import Tooltip from "components/Tooltip.astro";
|
import Tooltip from "components/Tooltip.astro";
|
||||||
import Button from "components/Button.astro";
|
import Button from "components/Button.astro";
|
||||||
import { getI18n } from "translations/translations";
|
import { formatLocale } from "src/utils/format";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentLang: string;
|
currentLang: string;
|
||||||
|
@ -11,7 +11,6 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { currentLang, getPartialUrl, availableLanguages } = Astro.props;
|
const { currentLang, getPartialUrl, availableLanguages } = Astro.props;
|
||||||
const { formatLocale } = await getI18n(Astro.locals.currentLocale);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import ErrorMessage from "components/ErrorMessage.astro";
|
import ErrorMessage from "components/ErrorMessage.astro";
|
||||||
import { getI18n } from "translations/translations";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
doc: {
|
doc: {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import Metadata from "components/Metadata.astro";
|
import Metadata from "components/Metadata.astro";
|
||||||
import { getI18n } from "translations/translations";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|
|
@ -1,26 +1,33 @@
|
||||||
|
import type { WordingKey } from "src/i18n/wordings-keys";
|
||||||
import { cache } from "src/utils/cachedPayload";
|
import { cache } from "src/utils/cachedPayload";
|
||||||
import en from "./en.json";
|
|
||||||
import fr from "./fr.json";
|
|
||||||
import ja from "./ja.json";
|
|
||||||
|
|
||||||
import acceptLanguage from "accept-language";
|
export const defaultLocale = "en";
|
||||||
import { KeysTypes } from "src/shared/payload/payload-sdk";
|
|
||||||
|
|
||||||
type WordingKeys = keyof typeof en;
|
|
||||||
const translationFiles: Record<string, Record<WordingKeys, string>> = {
|
|
||||||
en,
|
|
||||||
fr,
|
|
||||||
ja,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getI18n = async (locale: string) => {
|
export const getI18n = async (locale: string) => {
|
||||||
const translations = translationFiles[locale];
|
|
||||||
|
|
||||||
const formatWithValues = (
|
const formatWithValues = (
|
||||||
templateName: string,
|
templateName: string,
|
||||||
template: string,
|
template: string,
|
||||||
values: Record<string, any>
|
values: Record<string, any>
|
||||||
): string => {
|
): string => {
|
||||||
|
const limitMatchToBalanceCurlyBraces = (
|
||||||
|
matchArray: RegExpMatchArray
|
||||||
|
): string => {
|
||||||
|
// Cut match as soon as curly braces are balanced.
|
||||||
|
const match = matchArray[0];
|
||||||
|
let curlyCount = 2;
|
||||||
|
let index = 2;
|
||||||
|
while (index < match.length && curlyCount > 0) {
|
||||||
|
if (match[index] === "{") {
|
||||||
|
curlyCount++;
|
||||||
|
}
|
||||||
|
if (match[index] === "}") {
|
||||||
|
curlyCount--;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return match.substring(0, index);
|
||||||
|
};
|
||||||
|
|
||||||
Object.entries(values).forEach(([key, value]) => {
|
Object.entries(values).forEach(([key, value]) => {
|
||||||
if (
|
if (
|
||||||
!template.match(new RegExp(`{{ ${key}\\+|{{ ${key}\\?|{{ ${key} }}`))
|
!template.match(new RegExp(`{{ ${key}\\+|{{ ${key}\\?|{{ ${key} }}`))
|
||||||
|
@ -111,118 +118,37 @@ export const getI18n = async (locale: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLocalizedMatch = <T extends { language: string }>(
|
const getLocalizedMatch = <T extends { language: string }>(
|
||||||
options: T[],
|
options: T[]
|
||||||
fallback: Omit<T, "language">
|
|
||||||
): Omit<T, "language"> & { language?: string } =>
|
): Omit<T, "language"> & { language?: string } =>
|
||||||
options.find(({ language }) => language === locale) ??
|
options.find(({ language }) => language === locale) ??
|
||||||
options.find(({ language }) => language === defaultLocale) ??
|
options.find(({ language }) => language === defaultLocale) ??
|
||||||
options[0] ?? {
|
options[0]!; // We will consider that there will always be at least one option.
|
||||||
...fallback,
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLocalizedKey = (
|
const t = (key: WordingKey, values: Record<string, any> = {}): string => {
|
||||||
keyType: KeysTypes,
|
const wording = cache.wordings.find(({ name }) => name === key);
|
||||||
keyId: string,
|
const fallbackString = `«${key}»`;
|
||||||
format: "short" | "default"
|
|
||||||
) => {
|
|
||||||
const category = cache.keys.find(
|
|
||||||
({ id, type }) => id === keyId && type === keyType
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!category) {
|
if (!wording) {
|
||||||
return "UNKNOWN";
|
return fallbackString;
|
||||||
}
|
|
||||||
if (!category.translations) {
|
|
||||||
return category.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const translation = getLocalizedMatch(category.translations, {
|
const matchingWording = getLocalizedMatch(wording.translations).name;
|
||||||
name: category.name,
|
return formatWithValues(key, matchingWording, values);
|
||||||
short: category.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
return format === "default" ? translation.name : translation.short;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
const getLocalizedUrl = (url: string): string => `/${locale}${url}`;
|
||||||
t: (key: WordingKeys, values: Record<string, any> = {}): string => {
|
|
||||||
if (translations && key in translations) {
|
|
||||||
return formatWithValues(key, translations[key]!, values);
|
|
||||||
}
|
|
||||||
return `«${key}»`;
|
|
||||||
},
|
|
||||||
getLocalizedUrl: (url: string): string => `/${locale}${url}`,
|
|
||||||
getLocalizedMatch,
|
|
||||||
formatTag: (id: string): string => {
|
|
||||||
const tag = cache.tags.find(({ slug }) => slug === id);
|
|
||||||
if (!tag) return "UNKNOWN";
|
|
||||||
return getLocalizedMatch(tag.translations, { name: tag.slug }).name;
|
|
||||||
},
|
|
||||||
formatTagsGroup: (id: string): string => {
|
|
||||||
const tag = cache.tagsGroups.find(({ slug }) => slug === id);
|
|
||||||
if (!tag) return "UNKNOWN";
|
|
||||||
return getLocalizedMatch(tag.translations, { name: tag.slug }).name;
|
|
||||||
},
|
|
||||||
formatCategory: (
|
|
||||||
id: string,
|
|
||||||
format: "short" | "default" = "default"
|
|
||||||
): string => getLocalizedKey(KeysTypes.Categories, id, format),
|
|
||||||
formatContentType: (id: string): string =>
|
|
||||||
getLocalizedKey(KeysTypes.Contents, id, "default"),
|
|
||||||
formatRecorder: (recorderId: string): string => {
|
|
||||||
const result = cache.recorders.find(({ id }) => id === recorderId);
|
|
||||||
|
|
||||||
if (!result) {
|
const formatTag = (id: string): string => {
|
||||||
return "UNKNOWN";
|
const tag = cache.tags.find(({ slug }) => slug === id);
|
||||||
}
|
if (!tag) return "UNKNOWN";
|
||||||
|
return getLocalizedMatch(tag.translations).name;
|
||||||
return result.username;
|
|
||||||
},
|
|
||||||
formatLocale: (code: string): string =>
|
|
||||||
cache.locales.find(({ id }) => id === code)?.name ?? code,
|
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
const formatTagsGroup = (id: string): string => {
|
||||||
const limitMatchToBalanceCurlyBraces = (
|
const tag = cache.tagsGroups.find(({ slug }) => slug === id);
|
||||||
matchArray: RegExpMatchArray
|
if (!tag) return "UNKNOWN";
|
||||||
): string => {
|
return getLocalizedMatch(tag.translations).name;
|
||||||
// Cut match as soon as curly braces are balanced.
|
};
|
||||||
const match = matchArray[0];
|
|
||||||
let curlyCount = 2;
|
return { t, getLocalizedMatch, getLocalizedUrl, formatTag, formatTagsGroup };
|
||||||
let index = 2;
|
|
||||||
while (index < match.length && curlyCount > 0) {
|
|
||||||
if (match[index] === "{") {
|
|
||||||
curlyCount++;
|
|
||||||
}
|
|
||||||
if (match[index] === "}") {
|
|
||||||
curlyCount--;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return match.substring(0, index);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Locale = string;
|
|
||||||
export const defaultLocale: Locale = "en";
|
|
||||||
|
|
||||||
export const getCurrentLocale = (pathname: string): Locale | undefined => {
|
|
||||||
for (const locale of cache.locales) {
|
|
||||||
if (pathname.startsWith(`/${locale.id}`)) {
|
|
||||||
return locale.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBestAcceptedLanguage = (
|
|
||||||
request: Request
|
|
||||||
): Locale | undefined => {
|
|
||||||
const header = request.headers.get("Accept-Language");
|
|
||||||
if (!header) return;
|
|
||||||
|
|
||||||
acceptLanguage.languages(cache.locales.map(({ id }) => id));
|
|
||||||
|
|
||||||
return (
|
|
||||||
acceptLanguage.get(request.headers.get("Accept-Language")) ?? undefined
|
|
||||||
);
|
|
||||||
};
|
};
|
|
@ -0,0 +1,53 @@
|
||||||
|
export type WordingKey =
|
||||||
|
| "global.siteName"
|
||||||
|
| "global.siteSubtitle"
|
||||||
|
| "home.title"
|
||||||
|
| "home.description"
|
||||||
|
| "home.aboutUsButton"
|
||||||
|
| "home.librarySection.title"
|
||||||
|
| "home.librarySection.description"
|
||||||
|
| "home.librarySection.button"
|
||||||
|
| "home.chroniclesSection.title"
|
||||||
|
| "home.chroniclesSection.description"
|
||||||
|
| "home.changesSection.title"
|
||||||
|
| "home.changesSection.description"
|
||||||
|
| "home.changesSection.button"
|
||||||
|
| "home.moreSection.title"
|
||||||
|
| "home.moreSection.description"
|
||||||
|
| "home.linksSection.title"
|
||||||
|
| "home.linksSection.description"
|
||||||
|
| "settings.title"
|
||||||
|
| "settings.theme.title"
|
||||||
|
| "settings.theme.description"
|
||||||
|
| "settings.language.title"
|
||||||
|
| "settings.language.description"
|
||||||
|
| "settings.currency.title"
|
||||||
|
| "settings.currency.description"
|
||||||
|
| "header.topbar.search.tooltip"
|
||||||
|
| "header.topbar.theme.tooltip"
|
||||||
|
| "header.topbar.language.tooltip"
|
||||||
|
| "header.topbar.currency.tooltip"
|
||||||
|
| "global.theme.light"
|
||||||
|
| "global.theme.auto"
|
||||||
|
| "global.theme.dark"
|
||||||
|
| "footer.links.home.title"
|
||||||
|
| "footer.links.timeline.title"
|
||||||
|
| "footer.links.timeline.subtitle"
|
||||||
|
| "footer.links.gallery.title"
|
||||||
|
| "footer.links.gallery.subtitle"
|
||||||
|
| "footer.links.videos.title"
|
||||||
|
| "footer.links.videos.subtitle"
|
||||||
|
| "footer.links.webArchives.title"
|
||||||
|
| "footer.links.webArchives.subtitle"
|
||||||
|
| "footer.socials.discord.title"
|
||||||
|
| "footer.socials.discord.subtitle"
|
||||||
|
| "footer.socials.twitter.title"
|
||||||
|
| "footer.socials.twitter.subtitle"
|
||||||
|
| "footer.socials.github.title"
|
||||||
|
| "footer.socials.github.subtitle"
|
||||||
|
| "footer.socials.contact.title"
|
||||||
|
| "footer.socials.contact.subtitle"
|
||||||
|
| "footer.license.description"
|
||||||
|
| "footer.license.icons.tooltip"
|
||||||
|
| "footer.disclaimer"
|
||||||
|
| "header.nav.parentPages.label";
|
|
@ -1,18 +1,9 @@
|
||||||
import { defineMiddleware, sequence } from "astro:middleware";
|
import { defineMiddleware, sequence } from "astro:middleware";
|
||||||
import {
|
import { cache } from "src/utils/cachedPayload";
|
||||||
defaultLocale,
|
import acceptLanguage from "accept-language";
|
||||||
getCurrentLocale,
|
import type { AstroCookies } from "astro";
|
||||||
getBestAcceptedLanguage,
|
import { z } from "astro:content";
|
||||||
} from "translations/translations";
|
import { defaultLocale } from "src/i18n/i18n";
|
||||||
import {
|
|
||||||
CookieKeys,
|
|
||||||
getCookieCurrency,
|
|
||||||
getCookieLocale,
|
|
||||||
getCookieTheme,
|
|
||||||
isValidCurrency,
|
|
||||||
isValidLocale,
|
|
||||||
themeSchema,
|
|
||||||
} from "src/utils/cookies";
|
|
||||||
|
|
||||||
const getAbsoluteLocaleUrl = (locale: string, url: string) =>
|
const getAbsoluteLocaleUrl = (locale: string, url: string) =>
|
||||||
`/${locale}${url}`;
|
`/${locale}${url}`;
|
||||||
|
@ -130,3 +121,80 @@ export const onRequest = sequence(
|
||||||
localeNegotiator,
|
localeNegotiator,
|
||||||
provideLocalsToRequest
|
provideLocalsToRequest
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* LOCALE */
|
||||||
|
|
||||||
|
const getCurrentLocale = (pathname: string): string | undefined => {
|
||||||
|
for (const locale of cache.locales) {
|
||||||
|
if (pathname.startsWith(`/${locale.id}`)) {
|
||||||
|
return locale.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBestAcceptedLanguage = (request: Request): string | undefined => {
|
||||||
|
const header = request.headers.get("Accept-Language");
|
||||||
|
if (!header) return;
|
||||||
|
|
||||||
|
acceptLanguage.languages(cache.locales.map(({ id }) => id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
acceptLanguage.get(request.headers.get("Accept-Language")) ?? undefined
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* COOKIES */
|
||||||
|
|
||||||
|
export enum CookieKeys {
|
||||||
|
Currency = "al_pref_currency",
|
||||||
|
Theme = "al_pref_theme",
|
||||||
|
Languages = "al_pref_languages",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themeSchema = z.enum(["dark", "light", "auto"]);
|
||||||
|
|
||||||
|
export const getCookieLocale = (cookies: AstroCookies): string | undefined => {
|
||||||
|
const cookie = cookies.get(CookieKeys.Languages);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = cookie?.json();
|
||||||
|
const result = z.array(z.string()).nonempty().safeParse(json);
|
||||||
|
if (result.success && isValidLocale(result.data[0])) {
|
||||||
|
return result.data[0];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCookieCurrency = (
|
||||||
|
cookies: AstroCookies
|
||||||
|
): string | undefined => {
|
||||||
|
const cookieValue = cookies.get(CookieKeys.Currency)?.value;
|
||||||
|
return isValidCurrency(cookieValue) ? cookieValue : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCookieTheme = (
|
||||||
|
cookies: AstroCookies
|
||||||
|
): z.infer<typeof themeSchema> | undefined => {
|
||||||
|
const cookieValue = cookies.get(CookieKeys.Theme)?.value;
|
||||||
|
const result = themeSchema.safeParse(cookieValue);
|
||||||
|
return result.success ? result.data : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isValidCurrency = (
|
||||||
|
currency: string | null | undefined
|
||||||
|
): currency is string =>
|
||||||
|
currency !== null &&
|
||||||
|
currency != undefined &&
|
||||||
|
cache.currencies.includes(currency);
|
||||||
|
|
||||||
|
export const isValidLocale = (
|
||||||
|
locale: string | null | undefined
|
||||||
|
): locale is string =>
|
||||||
|
locale !== null &&
|
||||||
|
locale != undefined &&
|
||||||
|
cache.locales.map(({ id }) => id).includes(locale);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import { payload } from "src/shared/payload/payload-sdk";
|
import { payload } from "src/shared/payload/payload-sdk";
|
||||||
import { getI18n } from "translations/translations";
|
|
||||||
import CategoryCard from "./CategoryCard.astro";
|
import CategoryCard from "./CategoryCard.astro";
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
const folders = await payload.getRootFolders();
|
const folders = await payload.getRootFolders();
|
||||||
const { getLocalizedUrl, getLocalizedMatch } = await getI18n(
|
const { getLocalizedUrl, getLocalizedMatch } = await getI18n(
|
||||||
|
@ -21,7 +21,7 @@ const { getLocalizedUrl, getLocalizedMatch } = await getI18n(
|
||||||
? { dark: darkThumbnail.url, light: lightThumbnail.url }
|
? { dark: darkThumbnail.url, light: lightThumbnail.url }
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
name={getLocalizedMatch(translations, { name: slug }).name}
|
name={getLocalizedMatch(translations).name}
|
||||||
href={getLocalizedUrl(`/folders/${slug}`)}
|
href={getLocalizedUrl(`/folders/${slug}`)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
import { Collections } from "src/shared/payload/payload-sdk";
|
||||||
|
import { refreshWordings } from "src/utils/cachedPayload";
|
||||||
|
|
||||||
|
const auth = Astro.request.headers.get("Authorization");
|
||||||
|
|
||||||
|
if (auth !== `Bearer ${import.meta.env.WEB_HOOK_TOKEN}`) {
|
||||||
|
return new Response(null, { status: 403, statusText: "Forbidden" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionSlug = Astro.request.headers.get("Collection");
|
||||||
|
|
||||||
|
switch (collectionSlug) {
|
||||||
|
case Collections.Wordings:
|
||||||
|
await refreshWordings();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return new Response(null, { status: 400, statusText: "Bad Request" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null, { status: 200, statusText: "Ok" });
|
||||||
|
---
|
|
@ -1,13 +1,13 @@
|
||||||
---
|
---
|
||||||
import RichText from "components/RichText/RichText.astro";
|
import RichText from "components/RichText/RichText.astro";
|
||||||
import { payload, type EndpointPage } from "src/shared/payload/payload-sdk";
|
import { payload, type EndpointPage } from "src/shared/payload/payload-sdk";
|
||||||
import { getI18n } from "translations/translations";
|
|
||||||
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
|
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
|
||||||
import MasoTarget from "components/Maso/MasoTarget.astro";
|
import MasoTarget from "components/Maso/MasoTarget.astro";
|
||||||
import TagGroups from "components/TagGroups.astro";
|
import TagGroups from "components/TagGroups.astro";
|
||||||
import TableOfContent from "components/TableOfContent/TableOfContent.astro";
|
import TableOfContent from "components/TableOfContent/TableOfContent.astro";
|
||||||
import LanguageOverride from "components/LanguageOverride.astro";
|
import LanguageOverride from "components/LanguageOverride.astro";
|
||||||
import Credits from "components/Credits.astro";
|
import Credits from "components/Credits.astro";
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
export const partial = true;
|
export const partial = true;
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ const page = Astro.props.page ?? (await payload.getPage(slug));
|
||||||
const { getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
const { getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
||||||
const { getLocalizedMatch } = await getI18n(lang);
|
const { getLocalizedMatch } = await getI18n(lang);
|
||||||
|
|
||||||
const translation = getLocalizedMatch(page.translations, { title: slug });
|
const translation = getLocalizedMatch(page.translations);
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
---
|
---
|
||||||
import AppLayout from "components/AppLayout/AppLayout.astro";
|
import AppLayout from "components/AppLayout/AppLayout.astro";
|
||||||
import { payload } from "src/shared/payload/payload-sdk";
|
import { payload } from "src/shared/payload/payload-sdk";
|
||||||
import { getI18n } from "translations/translations";
|
|
||||||
import RichText from "components/RichText/RichText.astro";
|
import RichText from "components/RichText/RichText.astro";
|
||||||
import FoldersSection from "./_components/FoldersSection.astro";
|
import FoldersSection from "./_components/FoldersSection.astro";
|
||||||
import { fetchOr404 } from "src/utils/responses";
|
import { fetchOr404 } from "src/utils/responses";
|
||||||
import ErrorMessage from "components/ErrorMessage.astro";
|
import ErrorMessage from "components/ErrorMessage.astro";
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
const { slug } = Astro.params;
|
const { slug } = Astro.params;
|
||||||
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(
|
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(
|
||||||
|
@ -16,7 +16,7 @@ const folder = await fetchOr404(() => payload.getFolder(slug!));
|
||||||
if (folder instanceof Response) {
|
if (folder instanceof Response) {
|
||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
const meta = getLocalizedMatch(folder.translations, { name: slug });
|
const meta = getLocalizedMatch(folder.translations);
|
||||||
|
|
||||||
// TODO: handle light and dark illustration for applayout
|
// TODO: handle light and dark illustration for applayout
|
||||||
---
|
---
|
||||||
|
@ -46,7 +46,7 @@ const meta = getLocalizedMatch(folder.translations, { name: slug });
|
||||||
getLocalizedMatch<{
|
getLocalizedMatch<{
|
||||||
language: string;
|
language: string;
|
||||||
name: string | undefined;
|
name: string | undefined;
|
||||||
}>(translations, { name: undefined }).name
|
}>(translations).name
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -58,16 +58,6 @@ const meta = getLocalizedMatch(folder.translations, { name: slug });
|
||||||
{
|
{
|
||||||
folder.files.map(({ relationTo, value }) => {
|
folder.files.map(({ relationTo, value }) => {
|
||||||
switch (relationTo) {
|
switch (relationTo) {
|
||||||
case "contents":
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
class="pressable"
|
|
||||||
href={getLocalizedUrl(`/contents/${value.slug}`)}
|
|
||||||
>
|
|
||||||
{value.slug}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "library-items":
|
case "library-items":
|
||||||
return <p>Library item not supported yet! {value.slug}</p>;
|
return <p>Library item not supported yet! {value.slug}</p>;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import type { EndpointFolderPreview } from "src/shared/payload/payload-sdk";
|
import type { EndpointFolderPreview } from "src/shared/payload/payload-sdk";
|
||||||
import FolderCard from "./FolderCard.astro";
|
import FolderCard from "./FolderCard.astro";
|
||||||
import { getI18n } from "translations/translations";
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string | undefined;
|
title?: string | undefined;
|
||||||
|
@ -25,7 +25,7 @@ const { getLocalizedUrl, getLocalizedMatch } = await getI18n(
|
||||||
{
|
{
|
||||||
folders.map(({ slug, translations, icon }) => (
|
folders.map(({ slug, translations, icon }) => (
|
||||||
<FolderCard
|
<FolderCard
|
||||||
title={getLocalizedMatch(translations, { name: slug }).name}
|
title={getLocalizedMatch(translations).name}
|
||||||
icon={icon ? `material-symbols:${icon}` : undefined}
|
icon={icon ? `material-symbols:${icon}` : undefined}
|
||||||
href={getLocalizedUrl(`/folders/${slug}`)}
|
href={getLocalizedUrl(`/folders/${slug}`)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import AppLayout from "components/AppLayout/AppLayout.astro";
|
import AppLayout from "components/AppLayout/AppLayout.astro";
|
||||||
import Button from "components/Button.astro";
|
import Button from "components/Button.astro";
|
||||||
import { getI18n } from "../../../translations/translations";
|
|
||||||
import LibraryGrid from "./_components/LibraryGrid.astro";
|
import LibraryGrid from "./_components/LibraryGrid.astro";
|
||||||
import ChronicleCard from "./_components/ChronicleCard.astro";
|
import ChronicleCard from "./_components/ChronicleCard.astro";
|
||||||
import LinkCard from "./_components/LinkCard.astro";
|
import LinkCard from "./_components/LinkCard.astro";
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
|
|
||||||
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
||||||
---
|
---
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import AppLayout from "components/AppLayout/AppLayout.astro";
|
import AppLayout from "components/AppLayout/AppLayout.astro";
|
||||||
|
import { getI18n } from "src/i18n/i18n";
|
||||||
import { cache } from "src/utils/cachedPayload";
|
import { cache } from "src/utils/cachedPayload";
|
||||||
import { getI18n } from "translations/translations";
|
|
||||||
|
|
||||||
const { currentLocale, currentTheme, currentCurrency } = Astro.locals;
|
const { currentLocale, currentTheme, currentCurrency } = Astro.locals;
|
||||||
const { t } = await getI18n(currentLocale);
|
const { t } = await getI18n(currentLocale);
|
||||||
|
@ -37,21 +37,21 @@ const { t } = await getI18n(currentLocale);
|
||||||
href="?action-theme=dark"
|
href="?action-theme=dark"
|
||||||
data-astro-prefetch="tap"
|
data-astro-prefetch="tap"
|
||||||
>
|
>
|
||||||
{t("header.topbar.theme.dark")}
|
{t("global.theme.dark")}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class:list={{ current: currentTheme === "auto" }}
|
class:list={{ current: currentTheme === "auto" }}
|
||||||
href="?action-theme=auto"
|
href="?action-theme=auto"
|
||||||
data-astro-prefetch="tap"
|
data-astro-prefetch="tap"
|
||||||
>
|
>
|
||||||
{t("header.topbar.theme.auto")}
|
{t("global.theme.auto")}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class:list={{ current: currentTheme === "light" }}
|
class:list={{ current: currentTheme === "light" }}
|
||||||
href="?action-theme=light"
|
href="?action-theme=light"
|
||||||
data-astro-prefetch="tap"
|
data-astro-prefetch="tap"
|
||||||
>
|
>
|
||||||
{t("header.topbar.theme.light")}
|
{t("global.theme.light")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1586,10 +1586,6 @@ export type EndpointFolder = EndpointFolderPreview & {
|
||||||
relationTo: "library-items";
|
relationTo: "library-items";
|
||||||
value: LibraryItem;
|
value: LibraryItem;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
relationTo: "contents";
|
|
||||||
value: Content;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
relationTo: "pages";
|
relationTo: "pages";
|
||||||
value: Page;
|
value: Page;
|
||||||
|
@ -1609,30 +1605,6 @@ export type EndpointFolderPreview = {
|
||||||
darkThumbnail?: PayloadImage;
|
darkThumbnail?: PayloadImage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EndpointContent = {
|
|
||||||
slug: string;
|
|
||||||
thumbnail?: PayloadImage;
|
|
||||||
tagGroups: TagGroup[];
|
|
||||||
translations: {
|
|
||||||
language: string;
|
|
||||||
sourceLanguage: string;
|
|
||||||
pretitle?: string;
|
|
||||||
title: string;
|
|
||||||
subtitle?: string;
|
|
||||||
summary?: RichTextContent;
|
|
||||||
format: {
|
|
||||||
text?: {
|
|
||||||
content: RichTextContent;
|
|
||||||
toc: TableOfContentEntry[];
|
|
||||||
transcribers: string[];
|
|
||||||
translators: string[];
|
|
||||||
proofreaders: string[];
|
|
||||||
notes?: RichTextContent;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EndpointRecorder = {
|
export type EndpointRecorder = {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -1741,9 +1713,7 @@ export const payload = {
|
||||||
await (await request(payloadApiUrl(Collections.Languages, `all`))).json(),
|
await (await request(payloadApiUrl(Collections.Languages, `all`))).json(),
|
||||||
getCurrencies: async (): Promise<Currency[]> =>
|
getCurrencies: async (): Promise<Currency[]> =>
|
||||||
await (await request(payloadApiUrl(Collections.Currencies, `all`))).json(),
|
await (await request(payloadApiUrl(Collections.Currencies, `all`))).json(),
|
||||||
getKeys: async (): Promise<EndpointKey[]> =>
|
getWordings: async (): Promise<EndpointWording[]> =>
|
||||||
await (await request(payloadApiUrl(Collections.Keys, `all`))).json(),
|
|
||||||
getWordings: async (): Promise<EndpointKey[]> =>
|
|
||||||
await (await request(payloadApiUrl(Collections.Wordings, `all`))).json(),
|
await (await request(payloadApiUrl(Collections.Wordings, `all`))).json(),
|
||||||
getRecorders: async (): Promise<EndpointRecorder[]> =>
|
getRecorders: async (): Promise<EndpointRecorder[]> =>
|
||||||
await (await request(payloadApiUrl(Collections.Recorders, `all`))).json(),
|
await (await request(payloadApiUrl(Collections.Recorders, `all`))).json(),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
payload,
|
payload,
|
||||||
type EndpointKey,
|
|
||||||
type EndpointRecorder,
|
type EndpointRecorder,
|
||||||
type Language,
|
type Language,
|
||||||
type EndpointTag,
|
type EndpointTag,
|
||||||
|
@ -11,7 +10,6 @@ import {
|
||||||
type Cache = {
|
type Cache = {
|
||||||
locales: Language[];
|
locales: Language[];
|
||||||
currencies: string[];
|
currencies: string[];
|
||||||
keys: EndpointKey[];
|
|
||||||
recorders: EndpointRecorder[];
|
recorders: EndpointRecorder[];
|
||||||
tags: EndpointTag[];
|
tags: EndpointTag[];
|
||||||
tagsGroups: EndpointTagsGroup[];
|
tagsGroups: EndpointTagsGroup[];
|
||||||
|
@ -21,7 +19,6 @@ type Cache = {
|
||||||
const fetchNewData = async (): Promise<Cache> => ({
|
const fetchNewData = async (): Promise<Cache> => ({
|
||||||
locales: await payload.getLanguages(),
|
locales: await payload.getLanguages(),
|
||||||
currencies: (await payload.getCurrencies()).map(({ id }) => id),
|
currencies: (await payload.getCurrencies()).map(({ id }) => id),
|
||||||
keys: await payload.getKeys(),
|
|
||||||
recorders: await payload.getRecorders(),
|
recorders: await payload.getRecorders(),
|
||||||
tags: await payload.getTags(),
|
tags: await payload.getTags(),
|
||||||
tagsGroups: await payload.getTagsGroups(),
|
tagsGroups: await payload.getTagsGroups(),
|
||||||
|
@ -30,6 +27,10 @@ const fetchNewData = async (): Promise<Cache> => ({
|
||||||
|
|
||||||
export let cache = await fetchNewData();
|
export let cache = await fetchNewData();
|
||||||
|
|
||||||
|
export const refreshWordings = async () => {
|
||||||
|
cache.wordings = await payload.getWordings();
|
||||||
|
};
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
console.log("Refreshing cached Payload data");
|
console.log("Refreshing cached Payload data");
|
||||||
cache = await fetchNewData();
|
cache = await fetchNewData();
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
import type { AstroCookies } from "astro";
|
|
||||||
import { cache } from "src/utils/cachedPayload";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export enum CookieKeys {
|
|
||||||
Currency = "al_pref_currency",
|
|
||||||
Theme = "al_pref_theme",
|
|
||||||
Languages = "al_pref_languages",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const themeSchema = z.enum(["dark", "light", "auto"]);
|
|
||||||
|
|
||||||
export const getCookieLocale = (cookies: AstroCookies): string | undefined => {
|
|
||||||
const cookie = cookies.get(CookieKeys.Languages);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const json = cookie?.json();
|
|
||||||
const result = z.array(z.string()).nonempty().safeParse(json);
|
|
||||||
if (result.success && isValidLocale(result.data[0])) {
|
|
||||||
return result.data[0];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCookieCurrency = (
|
|
||||||
cookies: AstroCookies
|
|
||||||
): string | undefined => {
|
|
||||||
const cookieValue = cookies.get(CookieKeys.Currency)?.value;
|
|
||||||
return isValidCurrency(cookieValue) ? cookieValue : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCookieTheme = (
|
|
||||||
cookies: AstroCookies
|
|
||||||
): z.infer<typeof themeSchema> | undefined => {
|
|
||||||
const cookieValue = cookies.get(CookieKeys.Theme)?.value;
|
|
||||||
const result = themeSchema.safeParse(cookieValue);
|
|
||||||
return result.success ? result.data : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isValidCurrency = (
|
|
||||||
currency: string | null | undefined
|
|
||||||
): currency is string =>
|
|
||||||
currency !== null &&
|
|
||||||
currency != undefined &&
|
|
||||||
cache.currencies.includes(currency);
|
|
||||||
|
|
||||||
export const isValidLocale = (
|
|
||||||
locale: string | null | undefined
|
|
||||||
): locale is string =>
|
|
||||||
locale !== null &&
|
|
||||||
locale != undefined &&
|
|
||||||
cache.locales.map(({ id }) => id).includes(locale);
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { cache } from "src/utils/cachedPayload";
|
||||||
|
|
||||||
|
export const formatLocale = (code: string): string =>
|
||||||
|
cache.locales.find(({ id }) => id === code)?.name ?? code;
|
||||||
|
|
||||||
|
export const formatRecorder = (recorderId: string): string => {
|
||||||
|
const result = cache.recorders.find(({ id }) => id === recorderId);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.username;
|
||||||
|
};
|
|
@ -1,61 +0,0 @@
|
||||||
{
|
|
||||||
"global.siteName": "Accord’s Library",
|
|
||||||
"global.siteSubtitle": "Discover • Archive • Translate • Analyze",
|
|
||||||
"home.title": "Home",
|
|
||||||
"home.description": "We aim at archiving and translating all of <strong>Yoko Taro</strong>’s works.<br />Yoko Taro is a Japanese video game director and scenario writer. He is best-known for his involvement with the <strong>NieR</strong > and <strong>Drakengard</strong> series. To complement his games, Yoko Taro likes to publish side materials in the form of books, anime, manga, audio books, novellas, even theater plays.<br />These media can be very difficult to find. His work goes all the way back to 2003. Most of it was released solely in Japanese, and sometimes in short supply. So this is what we do here: <strong>discover, archive, translate, and analyze</strong>.",
|
|
||||||
"home.aboutUsButton": "Read more about us",
|
|
||||||
"home.librarySection.title": "The Library",
|
|
||||||
"home.librarySection.description": "Here you will find a list of IPs Yoko Taro worked on. Select one to discover all the media/content/articles that relates to this IP. Alternatively you can also browse all content and use tags and filters to narrow your search. <strong>Beware there can be spoilers.</strong>",
|
|
||||||
"home.librarySection.button": "Browse all content",
|
|
||||||
"home.chroniclesSection.title": "The Chronicles",
|
|
||||||
"home.chroniclesSection.description": "Interested in exploring the Yokoverse lore? Experience all events and content in chronological order. <strong>Beware there can be spoilers.</strong>",
|
|
||||||
"home.changesSection.title": "What’s new?",
|
|
||||||
"home.changesSection.description": "Here are the 10 most recently added/updated content. You can open the changelog to see all past changes.",
|
|
||||||
"home.changesSection.button": "Open the changelog",
|
|
||||||
"home.moreSection.title": "More content",
|
|
||||||
"home.moreSection.description": "The NieR and Drakengard series share a common timeline which you can explore it at the link bellow. Also we have gathered thousands of official artworks, videos, and notable web resources. <strong>Beware there can be spoilers.</strong>",
|
|
||||||
"home.linksSection.title": "Links",
|
|
||||||
"home.linksSection.description": "Do you have a <strong>question</strong>? Would like to share something with our <strong>community</strong>? Are you interested in <strong>contributing</strong> to this project? Whatever it is, you should find what you are looking for at the following links.",
|
|
||||||
|
|
||||||
"settings.title": "Settings",
|
|
||||||
"settings.theme.title": "Theme",
|
|
||||||
"settings.theme.description": "Switch between dark/light mode",
|
|
||||||
"settings.language.title": "Language",
|
|
||||||
"settings.language.description": "Select preferred language",
|
|
||||||
"settings.currency.title": "Currency",
|
|
||||||
"settings.currency.description": "Select preferred currency",
|
|
||||||
|
|
||||||
"header.topbar.search.tooltip": "Search on this website",
|
|
||||||
"header.topbar.theme.tooltip": "Switch between dark/light mode",
|
|
||||||
"header.topbar.theme.dark": "Dark",
|
|
||||||
"header.topbar.theme.auto": "Auto",
|
|
||||||
"header.topbar.theme.light": "Light",
|
|
||||||
"header.topbar.language.tooltip": "Select preferred language",
|
|
||||||
"header.topbar.currency.tooltip": "Select preferred currency",
|
|
||||||
|
|
||||||
"footer.links.home.title": "Home",
|
|
||||||
"footer.links.timeline.title": "Timeline",
|
|
||||||
"footer.links.timeline.subtitle": "{{ eraCount }} era{{ eraCount+,>1{s} }}, {{ eventCount }} event{{ eventCount+,>1{s} }}",
|
|
||||||
"footer.links.gallery.title": "Gallery",
|
|
||||||
"footer.links.gallery.subtitle": "{{ count }} images",
|
|
||||||
"footer.links.videos.title": "Videos",
|
|
||||||
"footer.links.videos.subtitle": "{{ count }} video{{ count+,>1{s} }}",
|
|
||||||
"footer.links.webArchives.title": "Web archives",
|
|
||||||
"footer.links.webArchives.subtitle": "{{ count }} archive{{ count+,>1{s} }}",
|
|
||||||
|
|
||||||
"footer.socials.discord.title": "Discord",
|
|
||||||
"footer.socials.discord.subtitle": "Join the community",
|
|
||||||
"footer.socials.twitter.title": "Twitter",
|
|
||||||
"footer.socials.twitter.subtitle": "Get the latest updates",
|
|
||||||
"footer.socials.github.title": "GitHub",
|
|
||||||
"footer.socials.github.subtitle": "Join the technical side",
|
|
||||||
"footer.socials.contact.title": "Contact",
|
|
||||||
"footer.socials.contact.subtitle": "Send us an email",
|
|
||||||
|
|
||||||
"footer.license.description": "This website’s content is made available under <a href=\"https://creativecommons.org/licenses/by-sa/4.0/\">CC-BY-SA</a> unless otherwise noted.",
|
|
||||||
"footer.license.icons.tooltip": "CC-BY-SA 4.0 License",
|
|
||||||
|
|
||||||
"footer.disclaimer": "<strong>Accord’s Library</strong> is not affiliated with or endorsed by <strong>SQUARE ENIX CO. LTD</strong>. All game assets and promotional materials belongs to <strong>© SQUARE ENIX CO. LTD</strong>.",
|
|
||||||
|
|
||||||
"header.nav.parentPages.label": "{{ count }} parent page{{ count+,>1{s} }}"
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
{
|
|
||||||
"global.siteName": "Accord’s Library",
|
|
||||||
"global.siteSubtitle": "Discover • Archive • Translate • Analyze",
|
|
||||||
"home.title": "Accueil",
|
|
||||||
"home.description": "Notre objectif est d'archiver et de traduire toutes les œuvres de <strong>Yoko Taro</strong>.<br />Yoko Taro est une réalisatrice et scénariste de jeux vidéo japonaise. Il est surtout connu pour son implication dans les séries <strong>NieR</strong> et <strong>Drakengard</strong>. Pour compléter ses jeux, Yoko Taro aime publier du matériel annexe sous forme de livres, d'animes, de mangas, de livres audio, de romans, voire de pièces de théâtre.<br />Ces médias peuvent être très difficiles à trouver. Son travail remonte à 2003. La majeure partie a été publiée uniquement en japonais, et parfois en quantité limitée. Voici donc ce que nous faisons ici : <strong>découvrir, archiver, traduire et analyser</strong>.",
|
|
||||||
"home.aboutUsButton": "En savoir plus sur nous",
|
|
||||||
"home.librarySection.title": "La bibliothèque",
|
|
||||||
"home.librarySection.description": "Vous trouverez ici une liste des IP sur lesquelles Yoko Taro a travaillé. Sélectionnez-en un pour découvrir tous les médias/contenus/articles liés à cette IP. <strong>Attention, il peut y avoir des spoilers.</strong>",
|
|
||||||
"home.moreSection.title": "Plus de contenu",
|
|
||||||
"home.moreSection.description": "Les séries NieR et Drakengard partagent une chronologie commune que vous pouvez explorer via le lien ci-dessous. Nous avons également rassemblé des milliers d’œuvres d’art officielles, de vidéos et de ressources Web notables. <strong>Attention, il peut y avoir des spoilers.</strong>",
|
|
||||||
"home.linksSection.title": "Liens",
|
|
||||||
"home.linksSection.description": "Avez-vous une <strong>question</strong> ? Vous souhaitez partager quelque chose avec notre <strong>communauté</strong> ? Êtes-vous intéressé à <strong>contribuer</strong> à ce projet ? Quoi qu’il en soit, vous devriez trouver ce que vous cherchez sur les liens suivants.",
|
|
||||||
|
|
||||||
"header.topbar.search.tooltip": "Rechercher sur ce site",
|
|
||||||
"header.topbar.theme.tooltip": "Basculer entre le mode sombre/clair",
|
|
||||||
"header.topbar.theme.dark": "Sombre",
|
|
||||||
"header.topbar.theme.auto": "Auto",
|
|
||||||
"header.topbar.theme.light": "Clair",
|
|
||||||
"header.topbar.language.tooltip": "Sélectionnez la langue préférée",
|
|
||||||
"header.topbar.currency.tooltip": "Sélectionnez la devise préférée",
|
|
||||||
|
|
||||||
"footer.links.home.title": "Accueil",
|
|
||||||
"footer.links.timeline.title": "Chronologie",
|
|
||||||
"footer.links.timeline.subtitle": "{{ eraCount }} époque{{ eraCount+,>1{s} }}, {{ eventCount }} évenement{{ eventCount+,>1{s} }}",
|
|
||||||
"footer.links.gallery.title": "Gallerie",
|
|
||||||
"footer.links.gallery.subtitle": "{{ count }} image{{ count+,>1{s} }}",
|
|
||||||
"footer.links.videos.title": "Vidéos",
|
|
||||||
"footer.links.videos.subtitle": "{{ count }} vidéo{{ count+,>1{s} }}",
|
|
||||||
"footer.links.webArchives.title": "Archives web",
|
|
||||||
"footer.links.webArchives.subtitle": "{{ count }} archive{{ count+,>1{s} }}",
|
|
||||||
|
|
||||||
"footer.socials.discord.title": "Discord",
|
|
||||||
"footer.socials.discord.subtitle": "Rejoindre la communauté",
|
|
||||||
"footer.socials.twitter.title": "Twitter",
|
|
||||||
"footer.socials.twitter.subtitle": "Connaitre les dernières nouvelles",
|
|
||||||
"footer.socials.github.title": "GitHub",
|
|
||||||
"footer.socials.github.subtitle": "Rejoindre l'équipe technique",
|
|
||||||
"footer.socials.contact.title": "Contact",
|
|
||||||
"footer.socials.contact.subtitle": "Nous contacter par email",
|
|
||||||
|
|
||||||
"footer.license.description": "Le contenu de ce site Web est disponible sous <a href=\"https://creativecommons.org/licenses/by-sa/4.0/\">CC-BY-SA</a>, sauf indication contraire.",
|
|
||||||
"footer.license.icons.tooltip": "Licence CC-BY-SA 4.0",
|
|
||||||
|
|
||||||
"footer.disclaimer": "<strong>Accord’s Library</strong> n'est ni affiliée ni approuvée par <strong>SQUARE ENIX CO. LTD</strong>. Tous les éléments du jeu et le matériel promotionnel appartiennent à <strong>© SQUARE ENIX CO. LTD</strong>."
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"global.siteName": "アコールの図書館"
|
|
||||||
}
|
|
Loading…
Reference in New Issue