diff --git a/astro.config.ts b/astro.config.ts
index a28a194..c622789 100644
--- a/astro.config.ts
+++ b/astro.config.ts
@@ -20,15 +20,6 @@ export default defineConfig({
},
}),
],
- i18n: {
- defaultLocale: "en",
- locales: ["en", "es", "fr", "ja", "pt", "zh"],
- routing: {
- prefixDefaultLocale: true,
- redirectToDefaultLocale: false,
- strategy: "pathname",
- },
- },
server: {
port: 12499,
host: true,
diff --git a/bun.lockb b/bun.lockb
index 021abbd..866c195 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 6fdf83a..35f151f 100644
--- a/package.json
+++ b/package.json
@@ -12,9 +12,13 @@
},
"dependencies": {
"@astrojs/check": "^0.4.1",
- "@astrojs/node": "^8.1.0",
- "astro": "^4.2.5",
- "astro-icon": "^1.0.3",
+ "@astrojs/node": "^8.2.0",
+ "@fontsource-variable/murecho": "^5.0.17",
+ "@fontsource-variable/vollkorn": "^5.0.19",
+ "accept-language": "^3.0.18",
+ "astro": "^4.3.0",
+ "astro-icon": "^1.0.4",
+ "htmx.org": "^1.9.10",
"tippy.js": "^6.3.7",
"ua-parser-js": "^1.0.37",
"zod": "^3.22.4"
@@ -25,9 +29,9 @@
"astro-meta-tags": "^0.2.1",
"autoprefixer": "^10.4.17",
"bun-types": "^1.0.25",
+ "npm-check-updates": "^16.14.14",
"postcss-preset-env": "^9.3.0",
"ts-node": "^10.9.2",
- "typescript": "^5.3.3",
- "npm-check-updates": "^16.14.14"
+ "typescript": "^5.3.3"
}
}
diff --git a/public/css/sanitize.min.css b/public/css/sanitize.min.css
deleted file mode 100644
index 9f9d08c..0000000
--- a/public/css/sanitize.min.css
+++ /dev/null
@@ -1 +0,0 @@
-*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:root){cursor:default;line-height:1.5;overflow-wrap:break-word;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%}:where(body){margin:0}:where(h1){font-size:2em;margin:0.67em 0}:where(dl,ol,ul):where(dl,ol,ul){margin:0}:where(hr){color:inherit;height:0}:where(nav):where(ol,ul){list-style-type:none;padding:0}:where(nav li)::before{content:"\200B";float:left}:where(pre){font-family:monospace, monospace;font-size:1em;overflow:auto}:where(abbr[title]){text-decoration:underline;text-decoration:underline dotted}:where(b,strong){font-weight:bolder}:where(code,kbd,samp){font-family:monospace, monospace;font-size:1em}:where(small){font-size:80%}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}:where(iframe){border-style:none}:where(svg:not([fill])){fill:currentColor}:where(table){border-collapse:collapse;border-color:inherit;text-indent:0}:where(button,input,select){margin:0}:where(button,[type="button" i],[type="reset" i],[type="submit" i]){-webkit-appearance:button}:where(fieldset){border:1px solid #a0a0a0}:where(progress){vertical-align:baseline}:where(textarea){margin:0;resize:vertical}:where([type="search" i]){-webkit-appearance:textfield;outline-offset:-2px}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-input-placeholder{color:inherit;opacity:0.54}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}:where(dialog){background-color:white;border:solid;color:black;height:-moz-fit-content;height:fit-content;left:0;margin:auto;padding:1em;position:absolute;right:0;width:-moz-fit-content;width:fit-content}:where(dialog:not([open])){display:none}:where(details > summary:first-of-type){display:list-item}:where([aria-busy="true" i]){cursor:progress}:where([aria-controls]){cursor:pointer}:where([aria-disabled="true" i],[disabled]){cursor:not-allowed}:where([aria-hidden="false" i][hidden]){display:initial}:where([aria-hidden="false" i][hidden]:not(:focus)){clip:rect(0, 0, 0, 0);position:absolute}
\ No newline at end of file
diff --git a/src/components/AppLayout/components/CurrencySelector.astro b/src/components/AppLayout/components/CurrencySelector.astro
index 976e153..d68df9b 100644
--- a/src/components/AppLayout/components/CurrencySelector.astro
+++ b/src/components/AppLayout/components/CurrencySelector.astro
@@ -9,17 +9,45 @@ interface Props {
}
const { withTitle, class: className } = Astro.props;
-const { t } = await getI18n(Astro.currentLocale!);
+const { t } = await getI18n(Astro.locals.currentLocale);
+
+const { currentCurrency } = Astro.locals;
---
+{
+ /* ------------------------------------------- HTML ------------------------------------------- */
+}
+
-
- USD
- EUR
-
+
+
+{
+ /* ------------------------------------------- CSS -------------------------------------------- */
+}
+
+
diff --git a/src/components/AppLayout/components/Footer.astro b/src/components/AppLayout/components/Footer.astro
index b1f2ae2..f2ce230 100644
--- a/src/components/AppLayout/components/Footer.astro
+++ b/src/components/AppLayout/components/Footer.astro
@@ -7,7 +7,7 @@ interface Props {
}
const { withLinks } = Astro.props;
-const { t } = await getI18n(Astro.currentLocale!);
+const { t } = await getI18n(Astro.locals.currentLocale);
const discordLabel = `${t("footer.socials.discord.title")} - ${t(
"footer.socials.discord.subtitle"
@@ -222,6 +222,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
@media (max-width: 35rem) {
grid-template-areas: "socials" "license";
border-left: unset;
+ padding-left: unset;
}
}
diff --git a/src/components/AppLayout/components/Html.astro b/src/components/AppLayout/components/Html.astro
index d6b4718..1ab7693 100644
--- a/src/components/AppLayout/components/Html.astro
+++ b/src/components/AppLayout/components/Html.astro
@@ -11,7 +11,7 @@ const userAgent = Astro.request.headers.get("user-agent") ?? "";
const parser = new UAParser(userAgent);
const isIOS = parser.getOS().name === "iOS";
-const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
+const { currentTheme } = Astro.locals;
/* -------------------------------------------- HTML -------------------------------------------- */
---
@@ -19,9 +19,9 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
@@ -251,10 +251,7 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
}
body {
- padding-top: clamp(12px, 3vmin, 24px);
- padding-left: clamp(24px, 4vw, 64px);
- padding-right: clamp(24px, 4vw, 64px);
- padding-bottom: clamp(24px, 6vmin, 48px);
+ padding: clamp(12px, 3vmin, 24px) clamp(24px, 4vw, 64px);
min-height: 100vb;
box-sizing: border-box;
display: flex;
diff --git a/src/components/AppLayout/components/LanguageSelector.astro b/src/components/AppLayout/components/LanguageSelector.astro
index 83a5d1b..d975f00 100644
--- a/src/components/AppLayout/components/LanguageSelector.astro
+++ b/src/components/AppLayout/components/LanguageSelector.astro
@@ -1,31 +1,55 @@
---
-import astroConfig from "astro.config";
import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro";
-import { getI18n } from "translations/translations";
+import { getI18n, locales } from "translations/translations";
interface Props {
withTitle?: boolean | undefined;
class?: string | undefined;
}
-const { withTitle, class:className } = Astro.props;
+const { withTitle, class: className } = Astro.props;
-const currentLocate = Astro.currentLocale ?? "en";
-const { t } = await getI18n(currentLocate);
+const { currentLocale } = Astro.locals;
+const { t } = await getI18n(currentLocale);
---
+{
+ /* ------------------------------------------- HTML ------------------------------------------- */
+}
+
-
+
{
- astroConfig.i18n?.locales.map((locale) => (
-
{locale.toString().toUpperCase()}
+ locales.map((locale) => (
+
+ {locale.toString().toUpperCase()}
+
))
}
-
+
+
+{
+ /* ------------------------------------------- CSS -------------------------------------------- */
+}
+
+
diff --git a/src/components/AppLayout/components/ThemeSelector.astro b/src/components/AppLayout/components/ThemeSelector.astro
index fbbda59..9854f90 100644
--- a/src/components/AppLayout/components/ThemeSelector.astro
+++ b/src/components/AppLayout/components/ThemeSelector.astro
@@ -3,15 +3,29 @@ import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "translations/translations";
-const { t } = await getI18n(Astro.currentLocale!);
+const { currentLocale, currentTheme } = Astro.locals;
+const { t } = await getI18n(currentLocale);
---
+{
+ /* ------------------------------------------- HTML ------------------------------------------- */
+}
+
-
- Dark
- Auto
- Light
-
+
+
+{
+ /* ------------------------------------------- CSS -------------------------------------------- */
+}
+
+
diff --git a/src/components/AppLayout/components/Topbar.astro b/src/components/AppLayout/components/Topbar.astro
index e2d060a..2260143 100644
--- a/src/components/AppLayout/components/Topbar.astro
+++ b/src/components/AppLayout/components/Topbar.astro
@@ -11,7 +11,7 @@ interface Props {
}
const { breadcrumb } = Astro.props;
-const { t } = await getI18n(Astro.currentLocale!);
+const { t } = await getI18n(Astro.locals.currentLocale);
---
{
diff --git a/src/env.d.ts b/src/env.d.ts
index acef35f..57548d5 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -1,2 +1,11 @@
///
///
+
+
+declare namespace App {
+ interface Locals {
+ currentLocale: import("translations/translations").Locale
+ currentTheme: "dark" | "auto" | "light"
+ currentCurrency: "usd" | "eur"
+ }
+}
\ No newline at end of file
diff --git a/src/middleware.ts b/src/middleware.ts
index 1399e38..b97941c 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,14 +1,18 @@
-import type { AstroCookies } from "astro";
import { defineMiddleware, sequence } from "astro:middleware";
import { z } from "zod";
-import astroConfig from "astro.config";
+import {
+ defaultLocale,
+ getCookiePreferredLocale,
+ getCurrentLocale,
+ getPreferredLocale,
+} from "translations/translations";
const cookieThemeSchema = z.enum(["dark", "light", "auto"]);
const getAbsoluteLocaleUrl = (locale: string, url: string) =>
`/${locale}${url}`;
-const redirection = (
+const redirect = (
redirectURL: string,
headers: Record = {}
): Response => {
@@ -19,30 +23,23 @@ const redirection = (
});
};
-export const langMiddleware = defineMiddleware(
- ({ cookies, preferredLocale, currentLocale, url }, next) => {
- const cookiePreferredLocale = getCookiePreferredLocale(cookies);
- const actionLang = url.searchParams.get("action-lang");
+const localeNegotiator = defineMiddleware(
+ ({ cookies, url, request }, next) => {
+ const currentLocale = getCurrentLocale(url.pathname);
+ const preferredLocale = getPreferredLocale(request);
- if (!currentLocale) {
- currentLocale = cookiePreferredLocale ?? preferredLocale ?? "en";
- const redirectURL = getAbsoluteLocaleUrl(currentLocale, url.pathname);
- return redirection(redirectURL);
+ if (url.pathname.startsWith("/api/")) {
+ return next();
}
- if (actionLang) {
- const pathnameWithoutLocale = url.pathname.substring(
- currentLocale.length + 1
- );
+ const cookiePreferredLocale = getCookiePreferredLocale(cookies);
+
+ if (!currentLocale) {
const redirectURL = getAbsoluteLocaleUrl(
- actionLang,
- pathnameWithoutLocale
+ cookiePreferredLocale ?? preferredLocale ?? defaultLocale,
+ url.pathname
);
- return redirection(redirectURL, {
- "Set-Cookie": `al_pref_languages=${JSON.stringify([
- actionLang,
- ])}; Path=/`,
- });
+ return redirect(redirectURL);
}
if (cookiePreferredLocale) {
@@ -54,7 +51,7 @@ export const langMiddleware = defineMiddleware(
cookiePreferredLocale,
pathnameWithoutLocale
);
- return redirection(redirectURL);
+ return redirect(redirectURL);
}
} else if (preferredLocale) {
if (preferredLocale !== currentLocale) {
@@ -65,60 +62,80 @@ export const langMiddleware = defineMiddleware(
preferredLocale,
pathnameWithoutLocale
);
- return redirection(redirectURL);
+ return redirect(redirectURL);
}
}
+
return next();
}
);
-export const headersMiddleware = defineMiddleware(
- async ({ currentLocale, url }, next) => {
- const actionTheme = url.searchParams.get("action-theme");
+const handleActionsSearchParams = defineMiddleware(async ({ url }, next) => {
+ // TODO: Verify locale typing
+ const actionLang = url.searchParams.get("action-lang");
+ if (actionLang) {
+ const currentLocale = getCurrentLocale(url.pathname);
+ const pathnameWithoutLocale = currentLocale
+ ? url.pathname.substring(currentLocale.length + 1)
+ : url.pathname;
+ const redirectURL = getAbsoluteLocaleUrl(actionLang, pathnameWithoutLocale);
+ return redirect(redirectURL, {
+ "Set-Cookie": `al_pref_languages=${JSON.stringify([actionLang])}; Path=/`,
+ });
+ }
- const verifiedActionTheme = cookieThemeSchema.safeParse(actionTheme);
+ // TODO: Verify currency typing
+ const actionCurrency = url.searchParams.get("action-currency");
+ if (actionCurrency) {
+ return redirect(url.pathname, {
+ "Set-Cookie": `al_pref_currency=${JSON.stringify(
+ actionCurrency
+ )}; Path=/`,
+ });
+ }
- if (verifiedActionTheme.success) {
- url.searchParams.delete("action-theme");
- if (verifiedActionTheme.data === "auto") {
- return redirection(url.toString(), {
- "Set-Cookie": `al_pref_theme=; Path=/; Expires=${new Date(0).toUTCString()}`,
- });
- }
- return redirection(url.toString(), {
- "Set-Cookie": `al_pref_theme=${verifiedActionTheme.data}; Path=/`,
+ const actionTheme = url.searchParams.get("action-theme");
+ const verifiedActionTheme = cookieThemeSchema.safeParse(actionTheme);
+
+ if (verifiedActionTheme.success) {
+ url.searchParams.delete("action-theme");
+ if (verifiedActionTheme.data === "auto") {
+ return redirect(url.pathname, {
+ "Set-Cookie": `al_pref_theme=; Path=/; Expires=${new Date(
+ 0
+ ).toUTCString()}`,
});
}
+ return redirect(url.pathname, {
+ "Set-Cookie": `al_pref_theme=${verifiedActionTheme.data}; Path=/`,
+ });
+ }
+
+ return next();
+});
+
+const addContentLanguageResponseHeader = defineMiddleware(
+ async ({ url }, next) => {
+ const currentLocale = getCurrentLocale(url.pathname);
const response = await next();
- if (currentLocale) {
+ if (response.status === 200 && currentLocale) {
response.headers.set("Content-Language", currentLocale);
}
return response;
}
);
-export const onRequest = sequence(headersMiddleware, langMiddleware);
+const provideLocalsToRequest = defineMiddleware(async ({ url, locals, cookies }, next) => {
+ locals.currentLocale = getCurrentLocale(url.pathname) ?? "en";
+ locals.currentCurrency = cookies.get("al_pref_currency")?.value ?? "usd"
+ locals.currentTheme = cookies.get("al_pref_theme")?.value ?? "auto"
+ return next();
+});
-const getCookiePreferredLocale = (
- cookies: AstroCookies
-): string | undefined => {
- const alPrefLanguages = cookies.get("al_pref_languages");
-
- try {
- const json = alPrefLanguages?.json();
- const result = z.array(z.string()).nonempty().safeParse(json);
- if (result.success) {
- for (const value of result.data) {
- if (astroConfig.i18n?.locales.includes(value)) {
- return value;
- }
- }
- }
- } catch (e) {
- console.error(e);
- return undefined;
- }
-
- return undefined;
-};
+export const onRequest = sequence(
+ addContentLanguageResponseHeader,
+ handleActionsSearchParams,
+ localeNegotiator,
+ provideLocalsToRequest
+);
diff --git a/src/pages/[locale]/drakengard/index.astro b/src/pages/[locale]/drakengard/index.astro
index c30ace4..e843f8c 100644
--- a/src/pages/[locale]/drakengard/index.astro
+++ b/src/pages/[locale]/drakengard/index.astro
@@ -1,6 +1,9 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
import FolderCard from "./_components/FolderCard.astro";
+import { getI18n } from "translations/translations";
+
+const {getLocalizedUrl} = await getI18n(Astro.locals.currentLocale)
---
{
@@ -19,62 +22,62 @@ import FolderCard from "./_components/FolderCard.astro";
diff --git a/src/pages/[locale]/index.astro b/src/pages/[locale]/index.astro
index aeefd1c..85d848d 100644
--- a/src/pages/[locale]/index.astro
+++ b/src/pages/[locale]/index.astro
@@ -6,7 +6,7 @@ import LinkCard from "../_components/LinkCard.astro";
import CategoryCard from "../_components/CategoryCard.astro";
import { getI18n } from "../../../translations/translations";
-const { t, getLocalizedUrl } = await getI18n(Astro.currentLocale!);
+const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
---
{
@@ -266,8 +266,8 @@ const { t, getLocalizedUrl } = await getI18n(Astro.currentLocale!);
line-height: 1;
margin: 0;
margin-top: -0.5em;
- font-size: 21.5px;
- font-weight: 700;
+ font-size: 21px;
+ font-weight: 600;
}
}
@@ -309,7 +309,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.currentLocale!);
& > section {
& > h2 {
font-family: var(--font-serif);
- font-size: 28px;
+ font-size: 30px;
}
& > p {
diff --git a/translations/en.json b/translations/en.json
index 41277c2..2e2c2c6 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -13,6 +13,9 @@
"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",
diff --git a/translations/fr.json b/translations/fr.json
index 2342847..8d6bfdb 100644
--- a/translations/fr.json
+++ b/translations/fr.json
@@ -1,11 +1,45 @@
{
+ "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 Yoko Taro.
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 NieR et Drakengard. 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.
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 : découvrir, archiver, traduire et analyser.",
+ "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. Attention, il peut y avoir des spoilers.",
+ "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. Attention, il peut y avoir des spoilers.",
+ "home.linksSection.title": "Liens",
+ "home.linksSection.description": "Avez-vous une question ? Vous souhaitez partager quelque chose avec notre communauté ? Êtes-vous intéressé à contribuer à 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}} époques, {{eventCount}} évenements",
+ "footer.links.timeline.subtitle": "{{ eraCount }} époque{{ eraCount+,>1{s} }}, {{ eventCount }} évenement{{ eventCount+,>1{s} }}",
"footer.links.gallery.title": "Gallerie",
- "footer.links.gallery.subtitle": "{{count}} images",
+ "footer.links.gallery.subtitle": "{{ count }} image{{ count+,>1{s} }}",
"footer.links.videos.title": "Vidéos",
- "footer.links.videos.subtitle": "{{count}} vidéos",
+ "footer.links.videos.subtitle": "{{ count }} vidéo{{ count+,>1{s} }}",
"footer.links.webArchives.title": "Archives web",
- "footer.links.webArchives.subtitle": "{{count}} archives"
+ "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 CC-BY-SA, sauf indication contraire.",
+ "footer.license.icons.tooltip": "Licence CC-BY-SA 4.0",
+
+ "footer.disclaimer": "Accord’s Library n'est ni affiliée ni approuvée par SQUARE ENIX CO. LTD. Tous les éléments du jeu et le matériel promotionnel appartiennent à © SQUARE ENIX CO. LTD."
}
diff --git a/translations/ja.json b/translations/ja.json
new file mode 100644
index 0000000..c6be582
--- /dev/null
+++ b/translations/ja.json
@@ -0,0 +1,3 @@
+{
+ "global.siteName": "アコールの図書館"
+}
\ No newline at end of file
diff --git a/translations/translations.ts b/translations/translations.ts
index 60b5936..a7d80b4 100644
--- a/translations/translations.ts
+++ b/translations/translations.ts
@@ -1,13 +1,20 @@
+import type { AstroCookies } from "astro";
import en from "./en.json";
+import fr from "./fr.json";
+import ja from "./ja.json"
+
+import acceptLanguage from 'accept-language';
+import { z } from "zod";
type WordingKeys = keyof typeof en;
+const translationFiles: Record> = {
+ en,
+ fr,
+ ja
+};
export const getI18n = async (locale: string) => {
- const file = Bun.file(`./translations/${locale}.json`, {
- type: "application/json",
- });
- const content = await file.text();
- const translations: Record = JSON.parse(content);
+ const translations = translationFiles[locale];
const formatWithValues = (
templateName: string,
@@ -105,7 +112,7 @@ export const getI18n = async (locale: string) => {
return {
t: (key: WordingKeys, values: Record = {}): string => {
- if (key in translations) {
+ if (translations && key in translations) {
return formatWithValues(key, translations[key]!, values);
}
return `«${key}»`;
@@ -132,3 +139,46 @@ const limitMatchToBalanceCurlyBraces = (
}
return match.substring(0, index);
};
+
+export const locales = ["en", "es", "fr", "ja", "pt", "zh"] as const;
+acceptLanguage.languages([...locales]);
+
+export type Locale = (typeof locales)[number];
+
+export const defaultLocale: Locale = "en";
+
+export const getCurrentLocale = (pathname: string): Locale | undefined => {
+ for (const locale of locales) {
+ if (pathname.startsWith(`/${locale}`)) {
+ return locale;
+ }
+ }
+ return undefined;
+};
+
+export const getPreferredLocale = (request: Request): Locale | undefined => {
+ return acceptLanguage.get(request.headers.get("Accept-Language")) as Locale | null ?? undefined;
+};
+
+export const getCookiePreferredLocale = (
+ cookies: AstroCookies
+): string | undefined => {
+ const alPrefLanguages = cookies.get("al_pref_languages");
+
+ try {
+ const json = alPrefLanguages?.json();
+ const result = z.array(z.string()).nonempty().safeParse(json);
+ if (result.success) {
+ for (const value of result.data) {
+ if (locales.includes(value as Locale)) {
+ return value;
+ }
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ return undefined;
+ }
+
+ return undefined;
+};