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

BIN
bun.lockb

Binary file not shown.

View File

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

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 { t } = await getI18n(Astro.currentLocale!);
const { t } = await getI18n(Astro.locals.currentLocale);
const { currentCurrency } = Astro.locals;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<Tooltip trigger="click" class={className}>
<Fragment slot="tooltip-content">
<a href="?action-currency=usd">USD</a>
<a href="?action-currency=eur">EUR</a>
</Fragment>
<div id="content" slot="tooltip-content">
<a
class:list={{ current: currentCurrency === "usd" }}
href="?action-currency=usd">USD</a
>
<a
class:list={{ current: currentCurrency === "eur" }}
href="?action-currency=eur">EUR</a
>
</div>
<Button
icon="material-symbols:currency-exchange"
title={withTitle ? "USD" : undefined}
title={withTitle ? currentCurrency.toUpperCase() : undefined}
ariaLabel={t("header.topbar.currency.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 { 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;
}
}

View File

@ -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;
<html
lang="en"
class:list={{
"manual-theme": prefTheme !== undefined,
"light-theme": prefTheme === "light",
"dark-theme": prefTheme === "dark",
"manual-theme": currentTheme !== "auto",
"light-theme": currentTheme === "light",
"dark-theme": currentTheme === "dark",
}}
>
<head>
@ -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;

View File

@ -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 ------------------------------------------- */
}
<Tooltip trigger="click" class={className}>
<Fragment slot="tooltip-content">
<div id="content" slot="tooltip-content">
{
astroConfig.i18n?.locales.map((locale) => (
<a href={`?action-lang=${locale}`}>{locale.toString().toUpperCase()}</a>
locales.map((locale) => (
<a
class:list={{ current: locale === currentLocale }}
href={`?action-lang=${locale}`}
>
{locale.toString().toUpperCase()}
</a>
))
}
</Fragment>
</div>
<Button
icon="material-symbols:translate"
title={withTitle ? currentLocate.toUpperCase() : undefined}
title={withTitle ? currentLocale.toUpperCase() : undefined}
ariaLabel={t("header.topbar.language.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 { getI18n } from "translations/translations";
const { t } = await getI18n(Astro.currentLocale!);
const { currentLocale, currentTheme } = Astro.locals;
const { t } = await getI18n(currentLocale);
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<Tooltip trigger="click">
<Fragment slot="tooltip-content">
<a href="?action-theme=dark">Dark</a>
<a href="?action-theme=auto">Auto</a>
<a href="?action-theme=light">Light</a>
</Fragment>
<div id="content" slot="tooltip-content">
<a
class:list={{ current: currentTheme === "dark" }}
href="?action-theme=dark">{t("header.topbar.theme.dark")}</a
>
<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
class="when-light-theme"
icon="material-symbols:sunny-outline"
@ -23,3 +37,19 @@ const { t } = await getI18n(Astro.currentLocale!);
ariaLabel={t("header.topbar.theme.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 { 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 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 { 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<string, string> = {}
): 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
);

View File

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

View File

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

View File

@ -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",

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.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 <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 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<string, Record<WordingKeys, string>> = {
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<string, string> = 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, any> = {}): 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;
};