Custom i18n routing handler

This commit is contained in:
DrMint 2024-02-01 21:00:55 +01:00
parent f2e433c3f7
commit f35064f2de
18 changed files with 327 additions and 134 deletions

View File

@ -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: { server: {
port: 12499, port: 12499,
host: true, host: true,

BIN
bun.lockb

Binary file not shown.

View File

@ -12,9 +12,13 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.4.1", "@astrojs/check": "^0.4.1",
"@astrojs/node": "^8.1.0", "@astrojs/node": "^8.2.0",
"astro": "^4.2.5", "@fontsource-variable/murecho": "^5.0.17",
"astro-icon": "^1.0.3", "@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", "tippy.js": "^6.3.7",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"zod": "^3.22.4" "zod": "^3.22.4"
@ -25,9 +29,9 @@
"astro-meta-tags": "^0.2.1", "astro-meta-tags": "^0.2.1",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"bun-types": "^1.0.25", "bun-types": "^1.0.25",
"npm-check-updates": "^16.14.14",
"postcss-preset-env": "^9.3.0", "postcss-preset-env": "^9.3.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.3.3", "typescript": "^5.3.3"
"npm-check-updates": "^16.14.14"
} }
} }

View File

@ -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}

View File

@ -9,17 +9,45 @@ interface Props {
} }
const { withTitle, class: className } = Astro.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 ------------------------------------------- */
}
<Tooltip trigger="click" class={className}> <Tooltip trigger="click" class={className}>
<Fragment slot="tooltip-content"> <div id="content" slot="tooltip-content">
<a href="?action-currency=usd">USD</a> <a
<a href="?action-currency=eur">EUR</a> class:list={{ current: currentCurrency === "usd" }}
</Fragment> href="?action-currency=usd">USD</a
>
<a
class:list={{ current: currentCurrency === "eur" }}
href="?action-currency=eur">EUR</a
>
</div>
<Button <Button
icon="material-symbols:currency-exchange" icon="material-symbols:currency-exchange"
title={withTitle ? "USD" : undefined} title={withTitle ? currentCurrency.toUpperCase() : undefined}
ariaLabel={t("header.topbar.currency.tooltip")} ariaLabel={t("header.topbar.currency.tooltip")}
/> />
</Tooltip> </Tooltip>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#content {
display: grid;
gap: 0.5em;
& > .current {
color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650);
}
}
</style>

View File

@ -7,7 +7,7 @@ interface Props {
} }
const { withLinks } = Astro.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( const discordLabel = `${t("footer.socials.discord.title")} - ${t(
"footer.socials.discord.subtitle" "footer.socials.discord.subtitle"
@ -222,6 +222,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
@media (max-width: 35rem) { @media (max-width: 35rem) {
grid-template-areas: "socials" "license"; grid-template-areas: "socials" "license";
border-left: unset; border-left: unset;
padding-left: unset;
} }
} }

View File

@ -11,7 +11,7 @@ const userAgent = Astro.request.headers.get("user-agent") ?? "";
const parser = new UAParser(userAgent); const parser = new UAParser(userAgent);
const isIOS = parser.getOS().name === "iOS"; const isIOS = parser.getOS().name === "iOS";
const prefTheme = Astro.cookies.get("al_pref_theme")?.value; const { currentTheme } = Astro.locals;
/* -------------------------------------------- HTML -------------------------------------------- */ /* -------------------------------------------- HTML -------------------------------------------- */
--- ---
@ -19,9 +19,9 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
<html <html
lang="en" lang="en"
class:list={{ class:list={{
"manual-theme": prefTheme !== undefined, "manual-theme": currentTheme !== "auto",
"light-theme": prefTheme === "light", "light-theme": currentTheme === "light",
"dark-theme": prefTheme === "dark", "dark-theme": currentTheme === "dark",
}} }}
> >
<head> <head>
@ -251,10 +251,7 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
} }
body { body {
padding-top: clamp(12px, 3vmin, 24px); padding: clamp(12px, 3vmin, 24px) clamp(24px, 4vw, 64px);
padding-left: clamp(24px, 4vw, 64px);
padding-right: clamp(24px, 4vw, 64px);
padding-bottom: clamp(24px, 6vmin, 48px);
min-height: 100vb; min-height: 100vb;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;

View File

@ -1,31 +1,55 @@
--- ---
import astroConfig from "astro.config";
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, locales } from "translations/translations";
interface Props { interface Props {
withTitle?: boolean | undefined; withTitle?: boolean | undefined;
class?: string | undefined; class?: string | undefined;
} }
const { withTitle, class:className } = Astro.props; const { withTitle, class: className } = Astro.props;
const currentLocate = Astro.currentLocale ?? "en"; const { currentLocale } = Astro.locals;
const { t } = await getI18n(currentLocate); const { t } = await getI18n(currentLocale);
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<Tooltip trigger="click" class={className}> <Tooltip trigger="click" class={className}>
<Fragment slot="tooltip-content"> <div id="content" slot="tooltip-content">
{ {
astroConfig.i18n?.locales.map((locale) => ( locales.map((locale) => (
<a href={`?action-lang=${locale}`}>{locale.toString().toUpperCase()}</a> <a
class:list={{ current: locale === currentLocale }}
href={`?action-lang=${locale}`}
>
{locale.toString().toUpperCase()}
</a>
)) ))
} }
</Fragment> </div>
<Button <Button
icon="material-symbols:translate" icon="material-symbols:translate"
title={withTitle ? currentLocate.toUpperCase() : undefined} title={withTitle ? currentLocale.toUpperCase() : undefined}
ariaLabel={t("header.topbar.language.tooltip")} ariaLabel={t("header.topbar.language.tooltip")}
/> />
</Tooltip> </Tooltip>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#content {
display: grid;
gap: 0.5em;
& > .current {
color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650);
}
}
</style>

View File

@ -3,15 +3,29 @@ 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 "translations/translations";
const { t } = await getI18n(Astro.currentLocale!); const { currentLocale, currentTheme } = Astro.locals;
const { t } = await getI18n(currentLocale);
--- ---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<Tooltip trigger="click"> <Tooltip trigger="click">
<Fragment slot="tooltip-content"> <div id="content" slot="tooltip-content">
<a href="?action-theme=dark">Dark</a> <a
<a href="?action-theme=auto">Auto</a> class:list={{ current: currentTheme === "dark" }}
<a href="?action-theme=light">Light</a> href="?action-theme=dark">{t("header.topbar.theme.dark")}</a
</Fragment> >
<a
class:list={{ current: currentTheme === "auto" }}
href="?action-theme=auto">{t("header.topbar.theme.auto")}</a
>
<a
class:list={{ current: currentTheme === "light" }}
href="?action-theme=light">{t("header.topbar.theme.light")}</a
>
</div>
<Button <Button
class="when-light-theme" class="when-light-theme"
icon="material-symbols:sunny-outline" icon="material-symbols:sunny-outline"
@ -23,3 +37,19 @@ const { t } = await getI18n(Astro.currentLocale!);
ariaLabel={t("header.topbar.theme.tooltip")} ariaLabel={t("header.topbar.theme.tooltip")}
/> />
</Tooltip> </Tooltip>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
#content {
display: grid;
gap: 0.5em;
& > .current {
color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650);
}
}
</style>

View File

@ -11,7 +11,7 @@ interface Props {
} }
const { breadcrumb } = Astro.props; const { breadcrumb } = Astro.props;
const { t } = await getI18n(Astro.currentLocale!); const { t } = await getI18n(Astro.locals.currentLocale);
--- ---
{ {

9
src/env.d.ts vendored
View File

@ -1,2 +1,11 @@
/// <reference path="../.astro/types.d.ts" /> /// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" /> /// <reference types="astro/client" />
declare namespace App {
interface Locals {
currentLocale: import("translations/translations").Locale
currentTheme: "dark" | "auto" | "light"
currentCurrency: "usd" | "eur"
}
}

View File

@ -1,14 +1,18 @@
import type { AstroCookies } from "astro";
import { defineMiddleware, sequence } from "astro:middleware"; import { defineMiddleware, sequence } from "astro:middleware";
import { z } from "zod"; 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 cookieThemeSchema = z.enum(["dark", "light", "auto"]);
const getAbsoluteLocaleUrl = (locale: string, url: string) => const getAbsoluteLocaleUrl = (locale: string, url: string) =>
`/${locale}${url}`; `/${locale}${url}`;
const redirection = ( const redirect = (
redirectURL: string, redirectURL: string,
headers: Record<string, string> = {} headers: Record<string, string> = {}
): Response => { ): Response => {
@ -19,30 +23,23 @@ const redirection = (
}); });
}; };
export const langMiddleware = defineMiddleware( const localeNegotiator = defineMiddleware(
({ cookies, preferredLocale, currentLocale, url }, next) => { ({ cookies, url, request }, next) => {
const cookiePreferredLocale = getCookiePreferredLocale(cookies); const currentLocale = getCurrentLocale(url.pathname);
const actionLang = url.searchParams.get("action-lang"); const preferredLocale = getPreferredLocale(request);
if (!currentLocale) { if (url.pathname.startsWith("/api/")) {
currentLocale = cookiePreferredLocale ?? preferredLocale ?? "en"; return next();
const redirectURL = getAbsoluteLocaleUrl(currentLocale, url.pathname);
return redirection(redirectURL);
} }
if (actionLang) { const cookiePreferredLocale = getCookiePreferredLocale(cookies);
const pathnameWithoutLocale = url.pathname.substring(
currentLocale.length + 1 if (!currentLocale) {
);
const redirectURL = getAbsoluteLocaleUrl( const redirectURL = getAbsoluteLocaleUrl(
actionLang, cookiePreferredLocale ?? preferredLocale ?? defaultLocale,
pathnameWithoutLocale url.pathname
); );
return redirection(redirectURL, { return redirect(redirectURL);
"Set-Cookie": `al_pref_languages=${JSON.stringify([
actionLang,
])}; Path=/`,
});
} }
if (cookiePreferredLocale) { if (cookiePreferredLocale) {
@ -54,7 +51,7 @@ export const langMiddleware = defineMiddleware(
cookiePreferredLocale, cookiePreferredLocale,
pathnameWithoutLocale pathnameWithoutLocale
); );
return redirection(redirectURL); return redirect(redirectURL);
} }
} else if (preferredLocale) { } else if (preferredLocale) {
if (preferredLocale !== currentLocale) { if (preferredLocale !== currentLocale) {
@ -65,60 +62,80 @@ export const langMiddleware = defineMiddleware(
preferredLocale, preferredLocale,
pathnameWithoutLocale pathnameWithoutLocale
); );
return redirection(redirectURL); return redirect(redirectURL);
} }
} }
return next(); return next();
} }
); );
export const headersMiddleware = defineMiddleware( const handleActionsSearchParams = defineMiddleware(async ({ url }, next) => {
async ({ currentLocale, url }, next) => { // TODO: Verify locale typing
const actionTheme = url.searchParams.get("action-theme"); 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=/`,
});
}
// 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=/`,
});
}
const actionTheme = url.searchParams.get("action-theme");
const verifiedActionTheme = cookieThemeSchema.safeParse(actionTheme); const verifiedActionTheme = cookieThemeSchema.safeParse(actionTheme);
if (verifiedActionTheme.success) { if (verifiedActionTheme.success) {
url.searchParams.delete("action-theme"); url.searchParams.delete("action-theme");
if (verifiedActionTheme.data === "auto") { if (verifiedActionTheme.data === "auto") {
return redirection(url.toString(), { return redirect(url.pathname, {
"Set-Cookie": `al_pref_theme=; Path=/; Expires=${new Date(0).toUTCString()}`, "Set-Cookie": `al_pref_theme=; Path=/; Expires=${new Date(
0
).toUTCString()}`,
}); });
} }
return redirection(url.toString(), { return redirect(url.pathname, {
"Set-Cookie": `al_pref_theme=${verifiedActionTheme.data}; Path=/`, "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(); const response = await next();
if (currentLocale) { if (response.status === 200 && currentLocale) {
response.headers.set("Content-Language", currentLocale); response.headers.set("Content-Language", currentLocale);
} }
return response; 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 = ( export const onRequest = sequence(
cookies: AstroCookies addContentLanguageResponseHeader,
): string | undefined => { handleActionsSearchParams,
const alPrefLanguages = cookies.get("al_pref_languages"); localeNegotiator,
provideLocalsToRequest
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;
};

View File

@ -1,6 +1,9 @@
--- ---
import AppLayout from "components/AppLayout/AppLayout.astro"; import AppLayout from "components/AppLayout/AppLayout.astro";
import FolderCard from "./_components/FolderCard.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";
<FolderCard <FolderCard
title="Games" title="Games"
icon="material-symbols:stadia-controller-outline" icon="material-symbols:stadia-controller-outline"
href="/drakengard/games" href={getLocalizedUrl("/drakengard/games")}
/> />
<FolderCard <FolderCard
title="Guidebooks" title="Guidebooks"
icon="material-symbols:menu-book-outline" icon="material-symbols:menu-book-outline"
href="/drakengard/guidebooks" href={getLocalizedUrl("/drakengard/guidebooks")}
/> />
<FolderCard <FolderCard
title="Manga" title="Manga"
icon="material-symbols:menu-book-outline" icon="material-symbols:menu-book-outline"
href="/drakengard/manga" href={getLocalizedUrl("/drakengard/manga")}
/> />
<FolderCard <FolderCard
title="Novels" title="Novels"
icon="material-symbols:menu-book-outline" icon="material-symbols:menu-book-outline"
href="/drakengard/novels" href={getLocalizedUrl("/drakengard/novels")}
/> />
<FolderCard <FolderCard
title="Novellas" title="Novellas"
icon="material-symbols:menu-book-outline" icon="material-symbols:menu-book-outline"
href="/drakengard/novellas " href={getLocalizedUrl("/drakengard/novellas")}
/> />
<FolderCard <FolderCard
title="Characters" title="Characters"
icon="material-symbols:person-outline" icon="material-symbols:person-outline"
href="/drakengard/characters" href={getLocalizedUrl("/drakengard/characters")}
/> />
<FolderCard <FolderCard
title="Places" title="Places"
icon="material-symbols:location-on-outline" icon="material-symbols:location-on-outline"
href="/drakengard/places" href={getLocalizedUrl("/drakengard/places")}
/> />
<FolderCard <FolderCard
title="Enemies" title="Enemies"
icon="material-symbols:sentiment-extremely-dissatisfied-outline" icon="material-symbols:sentiment-extremely-dissatisfied-outline"
href="/drakengard/enemies" href={getLocalizedUrl("/drakengard/enemies")}
/> />
<FolderCard <FolderCard
title="Terms" title="Terms"
icon="material-symbols:indeterminate-question-box" icon="material-symbols:indeterminate-question-box"
href="/drakengard/terms" href={getLocalizedUrl("/drakengard/terms")}
/> />
<FolderCard <FolderCard
title="Weapons" title="Weapons"
icon="material-symbols:swords-outline" icon="material-symbols:swords-outline"
href="/drakengard/weapons" href={getLocalizedUrl("/drakengard/weapons")}
/> />
<FolderCard <FolderCard
title="Timeline" title="Timeline"
icon="material-symbols:calendar-month-outline" icon="material-symbols:calendar-month-outline"
href="/drakengard/timeline" href={getLocalizedUrl("/drakengard/timeline")}
/> />
<FolderCard <FolderCard
title="News" title="News"
icon="material-symbols:newspaper" icon="material-symbols:newspaper"
href="/drakengard/news" href={getLocalizedUrl("/drakengard/news")}
/> />
</div> </div>
</AppLayout> </AppLayout>

View File

@ -6,7 +6,7 @@ import LinkCard from "../_components/LinkCard.astro";
import CategoryCard from "../_components/CategoryCard.astro"; import CategoryCard from "../_components/CategoryCard.astro";
import { getI18n } from "../../../translations/translations"; 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; line-height: 1;
margin: 0; margin: 0;
margin-top: -0.5em; margin-top: -0.5em;
font-size: 21.5px; font-size: 21px;
font-weight: 700; font-weight: 600;
} }
} }
@ -309,7 +309,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.currentLocale!);
& > section { & > section {
& > h2 { & > h2 {
font-family: var(--font-serif); font-family: var(--font-serif);
font-size: 28px; font-size: 30px;
} }
& > p { & > p {

View File

@ -13,6 +13,9 @@
"header.topbar.search.tooltip": "Search on this website", "header.topbar.search.tooltip": "Search on this website",
"header.topbar.theme.tooltip": "Switch between dark/light mode", "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.language.tooltip": "Select preferred language",
"header.topbar.currency.tooltip": "Select preferred currency", "header.topbar.currency.tooltip": "Select preferred currency",

View File

@ -1,11 +1,45 @@
{ {
"global.siteName": "Accords 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 dart 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 quil 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.home.title": "Accueil",
"footer.links.timeline.title": "Chronologie", "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.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.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.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 <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>Accords 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>."
} }

3
translations/ja.json Normal file
View File

@ -0,0 +1,3 @@
{
"global.siteName": "アコールの図書館"
}

View File

@ -1,13 +1,20 @@
import type { AstroCookies } from "astro";
import en from "./en.json"; 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; 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 file = Bun.file(`./translations/${locale}.json`, { const translations = translationFiles[locale];
type: "application/json",
});
const content = await file.text();
const translations: Record<string, string> = JSON.parse(content);
const formatWithValues = ( const formatWithValues = (
templateName: string, templateName: string,
@ -105,7 +112,7 @@ export const getI18n = async (locale: string) => {
return { return {
t: (key: WordingKeys, values: Record<string, any> = {}): string => { t: (key: WordingKeys, values: Record<string, any> = {}): string => {
if (key in translations) { if (translations && key in translations) {
return formatWithValues(key, translations[key]!, values); return formatWithValues(key, translations[key]!, values);
} }
return `«${key}»`; return `«${key}»`;
@ -132,3 +139,46 @@ const limitMatchToBalanceCurlyBraces = (
} }
return match.substring(0, index); 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;
};