Added custom translation format

This commit is contained in:
DrMint 2024-01-29 23:37:55 +01:00
parent 4408ac64d7
commit f2e433c3f7
18 changed files with 555 additions and 339 deletions

11
.vscode/launch.json vendored
View File

@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

143
README.md
View File

@ -1,54 +1,119 @@
# Astro Starter Kit: Basics
# Accord's Library
```sh
npm create astro@latest -- --template basics
```
## CSS Utility classes
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
- `when-js`: only display element if JavaScript is available
- `when-no-js`: only display element if JavaScript is unavailable
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
- `when-dark-theme`: only display element if the current theme is dark (manually or automatically)
- `when-light-theme`: only display element if the current theme is light (manually or automatically)
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
- `hide-scrollbar`: hide the element scrollbar
- `texture-dots`: add a background paper like texture to the element
## 🚀 Project Structure
- `font-serif`: by default, everything use sans-serif. Use this class to make the font serif.
Inside of your Astro project, you'll see the following folders and files:
## CSS Component classes
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── components/
│ │ └── Card.astro
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
- `pressable-icon`: used to make a SVG/Text look pressable
- `keycap`: used to make an element look like a pressable keycap
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
## CSS Global Variables
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
- `--color-base-X`: the current theme colors. X can be between 0 and 1000, available in increments of 50.
- `--font-serif`: by default, everything use sans-serif. Use this variable to make the font serif.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
## Translations
All commands are run from the root of the project, from a terminal:
For all the following exemples, the spaces within the double curly braces are important.
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
### Variables
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
Variables allow to embed strings or numbers within a translation.
In the JSON translation file:
`"home.greeting": "Hello {{ name }}!"`
If then you call:
`t("home.greeting", { name: "John" })`
It will produce
`Hello John!`
### Plural
In the JSON translation file:
`"videos": "{{ count }} video{{ count+,>1{s} }}"`
If then you call:
`t("videos", { count: 0 })`
`t("videos", { count: 1 })`
`t("videos", { count: 2 })`
It will produce
`0 video`
`1 video`
`2 videos`
You can provide multiple options inside a plural:
`"videos": "{{ count+,=0{No},=1{One},>1{{{ count }}} }} video{{ count+,>1{s} }}"`
If then you call:
`t("videos", { count: 0 })`
`t("videos", { count: 1 })`
`t("videos", { count: 2 })`
It will produce
`No video`
`One video`
`2 videos`
The following operators are supported: =, >, <
### Conditional
In the JSON translation file:
`"returnButton": "Return{{ x?, to {{ x }} }}"`
If then you call:
`t("returnButton", { x: "Home" })`
`t("returnButton", { x: undefined })`
`t("returnButton", { x: null })`
`t("returnButton", { x: "" })`
`t("returnButton", { x: 0 })`
It will produce
`Return to Home`
`Return`
`Return`
`Return to 0`
The condition is: `variable !== undefined && variable !== null && variable !== ""`
If the condition is met, the first value is used. If not, the second value is used. The first value is required. If the second value is omited, it will be consider as an empty string.
Here's an exemple where the second option is explicit. In the JSON translation file:
`"returnButton": "Return{{ x?, to {{ x }}, back }}"`
If then you call:
`t("returnButton", { x: "Home" })`
`t("returnButton", { x: undefined })`
It will produce
`Return to Home`
`Return back`

BIN
bun.lockb

Binary file not shown.

View File

@ -0,0 +1,25 @@
---
import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "translations/translations";
interface Props {
withTitle?: boolean | undefined;
class?: string | undefined;
}
const { withTitle, class: className } = Astro.props;
const { t } = await getI18n(Astro.currentLocale!);
---
<Tooltip trigger="click" class={className}>
<Fragment slot="tooltip-content">
<a href="?action-currency=usd">USD</a>
<a href="?action-currency=eur">EUR</a>
</Fragment>
<Button
icon="material-symbols:currency-exchange"
title={withTitle ? "USD" : undefined}
ariaLabel={t("header.topbar.currency.tooltip")}
/>
</Tooltip>

View File

@ -1,38 +1,56 @@
---
import { Icon } from "astro-icon/components";
import { getI18n } from "translations/translations";
interface Props {
withLinks: boolean;
}
const { withLinks } = Astro.props;
const { t } = await getI18n(Astro.currentLocale!);
const discordLabel = `${t("footer.socials.discord.title")} - ${t(
"footer.socials.discord.subtitle"
)}`;
const twitterLabel = `${t("footer.socials.twitter.title")} - ${t(
"footer.socials.twitter.subtitle"
)}`;
const githubLabel = `${t("footer.socials.github.title")} - ${t(
"footer.socials.github.subtitle"
)}`;
const contactLabel = `${t("footer.socials.contact.title")} - ${t(
"footer.socials.contact.subtitle"
)}`;
---
<footer>
{
withLinks && (
<div id="nav">
<p class="font-serif">Accords Library</p>
<p class="font-serif">{t("global.siteName")}</p>
<div>
<a href="/">
<Icon name="accords" />
<p>Home</p>
<p>{t("footer.links.home.title")}</p>
</a>
<a href="/timeline">
<Icon name="material-symbols:calendar-month-outline" />
<p>Timeline</p>
<p>{t("footer.links.timeline.title")}</p>
</a>
<a href="https://gallery.accords-library.com/posts">
<Icon name="material-symbols:perm-media-outline" />
<p>Gallery</p>
<p>{t("footer.links.gallery.title")}</p>
</a>
<a href="/videos">
<Icon name="material-symbols:movie-outline" />
<p>Videos</p>
<p>{t("footer.links.videos.title")}</p>
</a>
<a href="/archives">
<Icon name="material-symbols:folder-zip-outline" />
<p>Web archives</p>
<p>{t("footer.links.webArchives.title")}</p>
</a>
</div>
</div>
@ -40,13 +58,12 @@ const { withLinks } = Astro.props;
}
<div id="license" class:list={{ "with-links": withLinks }}>
<div id="license-section">
This websites content is made available under <a
href="https://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA</a
> unless otherwise noted.
<p set:html={t("footer.license.description")} />
<a
href="https://creativecommons.org/licenses/by-sa/4.0/"
id="common-creative"
aria-label="CC-BY-SA 4.0 License"
aria-label={t("footer.license.icons.tooltip")}
title={t("footer.license.icons.tooltip")}
class="pressable-icon"
>
<Icon name="creative-commons-brands" />
@ -61,29 +78,33 @@ const { withLinks } = Astro.props;
<a
href="/discord"
class="pressable-icon"
aria-label="Join the community"
aria-label={discordLabel}
title={discordLabel}
>
<Icon name="discord-brands" />
</a>
<a
href="https://twitter.com/AccordsLibrary"
class="pressable-icon"
aria-label="Get the latest updates"
aria-label={twitterLabel}
title={twitterLabel}
>
<Icon name="x-brands" />
</a>
<a
href="https://github.com/Accords-Library"
class="pressable-icon"
aria-label="Join the technical side"
aria-label={githubLabel}
title={githubLabel}
>
<Icon name="github-brands" />
</a>
<a
href="/contact"
aria-label="Send us an email"
class="pressable-icon"
aria-label={contactLabel}
title={contactLabel}
>
<Icon name="material-symbols:mail-outline" />
</a>
@ -91,13 +112,7 @@ const { withLinks } = Astro.props;
)
}
</div>
<div id="copyright">
<strong>Accords 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
>.
</div>
<div id="copyright" set:html={t("footer.disclaimer")} />
</footer>
<style>

View File

@ -94,6 +94,14 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
--texture-dots: url(/img/paper-dots.webp);
--texture-dots-blend: multiply;
& .when-light-theme {
display: initial !important;
}
& .when-dark-theme {
display: none !important;
}
}
&.dark-theme {
@ -130,6 +138,14 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
--texture-dots: url(/img/paper-dots-dark.webp);
--texture-dots-blend: overlay;
& .when-light-theme {
display: none !important;
}
& .when-dark-theme {
display: initial !important;
}
}
&:not(.manual-theme) {
@ -168,6 +184,14 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
--texture-dots: url(/img/paper-dots.webp);
--texture-dots-blend: multiply;
& .when-light-theme {
display: initial !important;
}
& .when-dark-theme {
display: none !important;
}
}
@media (prefers-color-scheme: dark) {
@ -204,14 +228,18 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
--texture-dots: url(/img/paper-dots-dark.webp);
--texture-dots-blend: overlay;
& .when-light-theme {
display: none !important;
}
& .when-dark-theme {
display: initial !important;
}
}
}
}
:root {
--font-serif: "Vollkorn", serif;
}
/* THEMING */
html,
@ -250,6 +278,10 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
font-family: "Vollkorn", serif;
}
:root {
--font-serif: "Vollkorn", serif;
}
button,
body {
font-family: "Zen Maru Gothic", sans-serif;
@ -257,7 +289,15 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
}
a {
color: var(--color-base-1000);
color: unset;
text-decoration: unset;
}
p {
& a {
color: var(--color-base-750);
text-decoration: underline dotted var(--color-base-650);
}
}
.texture-dots {
@ -270,6 +310,7 @@ const prefTheme = Astro.cookies.get("al_pref_theme")?.value;
.pressable-icon {
transition: 150ms color;
cursor: pointer;
&:hover {
color: var(--color-base-700);

View File

@ -0,0 +1,31 @@
---
import astroConfig from "astro.config";
import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "translations/translations";
interface Props {
withTitle?: boolean | undefined;
class?: string | undefined;
}
const { withTitle, class:className } = Astro.props;
const currentLocate = Astro.currentLocale ?? "en";
const { t } = await getI18n(currentLocate);
---
<Tooltip trigger="click" class={className}>
<Fragment slot="tooltip-content">
{
astroConfig.i18n?.locales.map((locale) => (
<a href={`?action-lang=${locale}`}>{locale.toString().toUpperCase()}</a>
))
}
</Fragment>
<Button
icon="material-symbols:translate"
title={withTitle ? currentLocate.toUpperCase() : undefined}
ariaLabel={t("header.topbar.language.tooltip")}
/>
</Tooltip>

View File

@ -0,0 +1,25 @@
---
import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "translations/translations";
const { t } = await getI18n(Astro.currentLocale!);
---
<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>
<Button
class="when-light-theme"
icon="material-symbols:sunny-outline"
ariaLabel={t("header.topbar.theme.tooltip")}
/>
<Button
class="when-dark-theme"
icon="material-symbols:dark-mode-outline"
ariaLabel={t("header.topbar.theme.tooltip")}
/>
</Tooltip>

View File

@ -1,15 +1,17 @@
---
import { Icon } from "astro-icon/components";
import Button from "components/Button.astro";
import Tooltip from "pages/_components/Tooltip.astro";
import astroConfig from "astro.config";
import ThemeSelector from "components/AppLayout/components/ThemeSelector.astro";
import LanguageSelector from "components/AppLayout/components/LanguageSelector.astro";
import CurrencySelector from "components/AppLayout/components/CurrencySelector.astro";
import { getI18n } from "translations/translations";
interface Props {
breadcrumb: { name: string; slug: string }[];
}
const { breadcrumb } = Astro.props;
const currentLocate = Astro.currentLocale ?? "en";
const { t } = await getI18n(Astro.currentLocale!);
---
{
@ -17,79 +19,43 @@ const currentLocate = Astro.currentLocale ?? "en";
}
<nav id="topbar">
<div id="breadcrumb" class="hide-scrollbar">
{
breadcrumb.length > 0 && (
<>
<a href="/">
<>
<Icon name="accords" width={16} height={16} />
<p>Home</p>
</>
</a>
{breadcrumb.map(({ name, slug }) => (
<>
<Icon
name="material-symbols:arrow-forward-ios"
width={12}
height={12}
/>
<a href={slug}>
<p>{name}</p>
</a>
</>
))}
</>
)
}
</div>
<div id="toolbar">
<Button icon="material-symbols:search" ariaLabel="Search on this website" />
<div class="separator"></div>
<Tooltip>
<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>
<Button
icon="material-symbols:sunny-outline"
ariaLabel="Switch between dark/light mode"
/>
</Tooltip>
<Button
class="m-only"
icon="material-symbols:translate"
ariaLabel="Select preferred language"
/>
<Button
class="m-only"
icon="material-symbols:currency-exchange"
ariaLabel="Select preferred currency"
/>
<Tooltip class="m-not">
<Fragment slot="tooltip-content">
{
astroConfig.i18n?.locales.map((locale) => (
<a href={`?action-lang=${locale}`}>
{locale.toString().toUpperCase()}
{
breadcrumb.length > 0 && (
<div id="breadcrumb" class="hide-scrollbar">
<a href="/">
<>
<Icon name="accords" width={16} height={16} />
<p>{t("home.title")}</p>
</>
</a>
{breadcrumb.map(({ name, slug }) => (
<>
<Icon
name="material-symbols:arrow-forward-ios"
width={12}
height={12}
/>
<a href={slug}>
<p>{name}</p>
</a>
))
}
</Fragment>
<Button
icon="material-symbols:translate"
title={currentLocate.toUpperCase()}
ariaLabel="Select preferred language"
/>
</Tooltip>
</>
))}
</div>
)
}
<div id="toolbar" class="when-js">
<Button
class="m-not"
icon="material-symbols:currency-exchange"
title="USD"
ariaLabel="Select preferred currency"
icon="material-symbols:search"
ariaLabel={t("header.topbar.search.tooltip")}
/>
<div class="separator"></div>
<ThemeSelector />
<LanguageSelector class="m-not" withTitle />
<LanguageSelector class="m-only" />
<CurrencySelector class="m-not" withTitle />
<CurrencySelector class="m-only" />
</div>
</nav>
@ -141,7 +107,7 @@ const currentLocate = Astro.currentLocale ?? "en";
& > #toolbar {
flex-grow: 1;
display: flex;
gap: clamp(4px, 4px + 1vw, 12px);
gap: 12px;
place-items: center;
justify-content: flex-end;
@ -164,8 +130,7 @@ const currentLocate = Astro.currentLocale ?? "en";
}
}
& > :global(.m-only),
& > :global(.s-only) {
& > :global(.m-only) {
display: none;
}
@ -177,22 +142,6 @@ const currentLocate = Astro.currentLocale ?? "en";
& > :global(.m-not) {
display: none;
}
@media (max-width: 28rem) {
& > :global(.s-only) {
display: flex;
}
& > :global(.s-not) {
display: none;
}
}
@media (max-width: 25rem) {
& > :global(.xs-not) {
display: none;
}
}
}
}
}
@ -200,7 +149,7 @@ const currentLocate = Astro.currentLocale ?? "en";
<script is:inline>
const breadcrumbElem = document.querySelector("nav#topbar > #breadcrumb");
breadcrumbElem.scrollTo({
breadcrumbElem?.scrollTo({
left: breadcrumbElem.scrollWidth,
behavior: "instant",
});

View File

@ -3,7 +3,7 @@ import { Icon } from "astro-icon/components";
interface Props {
id?: string;
title?: string;
title?: string | undefined;
icon?: string | string[];
class?: string;
ariaLabel?: string;
@ -23,6 +23,7 @@ const icons =
id={id}
class:list={[{ "with-title": !!title }, className]}
aria-label={ariaLabel}
title={ariaLabel}
>
{icons.map((cIcon) => <Icon name={cIcon} />)}
{title}
@ -62,7 +63,7 @@ const icons =
height: 1.2em;
}
svg {
> svg {
width: 1.5em;
height: 1.5em;
}

View File

@ -1,65 +0,0 @@
---
import Button from "components/Button.astro";
type ButtonProps = Parameters<typeof Button>[0] & {
href?: string;
};
interface Props {
buttons: ButtonProps[];
}
const { buttons } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
<div class="button-group">
{
buttons.map(({ href, ...otherProps }) =>
href ? (
<a href={href}>
<Button {...otherProps} />
</a>
) : (
<Button {...otherProps} />
)
)
}
</div>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<style>
.button-group {
display: flex;
gap: 0;
& > :global(*) {
&:is(button),
& > :global(button) {
border-radius: 0;
}
&:first-child:is(button),
&:first-child > :global(button) {
border-top-left-radius: 9999px;
border-bottom-left-radius: 9999px;
border-right: unset;
padding-right: 0.5em;
}
&:last-child:is(button),
&:last-child > :global(button) {
border-top-right-radius: 9999px;
border-bottom-right-radius: 9999px;
border-left: unset;
padding-left: 0.5em;
}
}
}
</style>

View File

@ -1,12 +1,13 @@
---
interface Props {
class?: string;
class?: string | undefined;
trigger?: string | undefined;
}
const { class: className } = Astro.props;
const { class: className, trigger = "mouseenter focus" } = Astro.props;
---
<tippy-tooltip class={className}>
<tippy-tooltip class={className} data-tippy-trigger={trigger}>
<template><slot name="tooltip-content" /></template>
<slot />
</tippy-tooltip>
@ -23,7 +24,6 @@ const { class: className } = Astro.props;
content: (ref) =>
ref.querySelector(":scope > template")?.innerHTML ?? "",
interactive: true,
trigger: "click",
});
}
}

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 } = await getI18n(Astro.currentLocale!);
const { t, getLocalizedUrl } = await getI18n(Astro.currentLocale!);
---
{
@ -22,24 +22,25 @@ const { t } = await getI18n(Astro.currentLocale!);
<div id="title" slot="header-title">
<Icon name="accords" />
<div>
<h1 class="font-serif">Accords Library</h1>
<p>Discover • Archive • Translate • Analyze</p>
<h1 class="font-serif">{t("global.siteName")}</h1>
<p>{t("global.siteSubtitle")}</p>
</div>
</div>
<div id="description" slot="header-description">
<p set:html={t("home_description")} />
<Button title="Read more about us" icon="material-symbols:left-click" />
<p set:html={t("home.description")} />
<a href={getLocalizedUrl("/about")}>
<Button
title={t("home.aboutUsButton")}
icon="material-symbols:left-click"
/>
</a>
</div>
<Fragment slot="main">
<div id="main">
<section id="library">
<h2>The Library</h2>
<p>
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.
<strong>Beware there can be spoilers.</strong>
</p>
<h2>{t("home.librarySection.title")}</h2>
<p set:html={t("home.librarySection.description")} />
<div class="grid">
<CategoryCard
img={{
@ -47,7 +48,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/dod.png",
}}
name="Drakengard"
href="/drakengard"
href={getLocalizedUrl("/drakengard")}
/>
<CategoryCard
img={{
@ -55,7 +56,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/dod2.png",
}}
name="Drakengard 2"
href="/drakengard-2"
href={getLocalizedUrl("/drakengard-2")}
/>
<CategoryCard
img={{
@ -63,7 +64,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/dod3.png",
}}
name="Drakengard 3"
href="/drakengard-3"
href={getLocalizedUrl("/drakengard-3")}
/>
<CategoryCard
img={{
@ -71,7 +72,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/dod1.3.png",
}}
name="Drakengard 1.3"
href="/drakengard-1-3"
href={getLocalizedUrl("/drakengard-1-3")}
/>
<CategoryCard
img={{
@ -79,7 +80,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/nier.png",
}}
name="NieR"
href="/nier"
href={getLocalizedUrl("/nier")}
/>
<CategoryCard
img={{
@ -87,7 +88,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/na.png",
}}
name="NieR:Automata"
href="/nier-automata"
href={getLocalizedUrl("/nier-automata")}
/>
<CategoryCard
img={{
@ -95,7 +96,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/nier-rein.png",
}}
name="NieR Re[in]carnation"
href="/nier-rein"
href={getLocalizedUrl("/nier-rein")}
/>
<CategoryCard
img={{
@ -103,7 +104,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/yorha.png",
}}
name="YoRHa"
href="/yorha"
href={getLocalizedUrl("/yorha")}
/>
<CategoryCard
img={{
@ -111,7 +112,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/yorha-boys.png",
}}
name="YoRHa Boys"
href="/yorha-boys"
href={getLocalizedUrl("/yorha-boys")}
/>
<CategoryCard
img={{
@ -119,7 +120,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/sino.png",
}}
name="SINoALICE"
href="/sinoalice"
href={getLocalizedUrl("/sinoalice")}
/>
<CategoryCard
img={{
@ -127,7 +128,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/404gamereset.png",
}}
name="404 Game Re:Set"
href="/404-game-reset"
href={getLocalizedUrl("/404-game-reset")}
/>
<CategoryCard
img={{
@ -135,7 +136,7 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/god-app.png",
}}
name="God App"
href="/god-app"
href={getLocalizedUrl("/god-app")}
/>
<CategoryCard
img={{
@ -143,82 +144,73 @@ const { t } = await getI18n(Astro.currentLocale!);
dark: "/img/categories/dark/voc.png",
}}
name="Voice of Cards"
href="/voice-of-cards"
href={getLocalizedUrl("/voice-of-cards")}
/>
<CategoryCard name="Others..." href="others" />
<CategoryCard name="Others..." href={getLocalizedUrl("/others")} />
</div>
</section>
<section>
<h2>More content</h2>
<p>
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>
</p>
<h2>{t("home.moreSection.title")}</h2>
<p set:html={t("home.moreSection.description")} />
<div class="grid">
<LinkCard
icon="material-symbols:calendar-month-outline"
title="Timeline"
subtitle="8 eras, 358 events"
href="/timeline"
title={t("footer.links.timeline.title")}
subtitle={t("footer.links.timeline.subtitle", {
eraCount: 8,
eventCount: 358,
})}
href={getLocalizedUrl("/timeline")}
/>
<LinkCard
icon="material-symbols:perm-media-outline"
title="Gallery"
subtitle="5,750 images"
title={t("footer.links.gallery.title")}
subtitle={t("footer.links.gallery.subtitle", { count: 5750 })}
href="https://gallery.accords-library.com/posts"
/>
<LinkCard
icon="material-symbols:movie-outline"
title="Videos"
subtitle="2,115 videos"
href="/videos"
title={t("footer.links.videos.title")}
subtitle={t("footer.links.videos.subtitle", { count: 2115 })}
href={getLocalizedUrl("/videos")}
/>
<LinkCard
icon="material-symbols:folder-zip-outline"
title="Web archives"
subtitle="20 archives"
href="/archives"
title={t("footer.links.webArchives.title")}
subtitle={t("footer.links.webArchives.subtitle", { count: 20 })}
href={getLocalizedUrl("/archives")}
/>
</div>
</section>
<section>
<h2>Links</h2>
<p>
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.
</p>
<h2>{t("home.linksSection.title")}</h2>
<p set:html={t("home.linksSection.description")} />
<div class="grid">
<LinkCard
icon="discord-brands"
title="Discord"
subtitle="Join the community"
title={t("footer.socials.discord.title")}
subtitle={t("footer.socials.discord.subtitle")}
href="/discord"
/>
<LinkCard
icon="x-brands"
title="Twitter"
subtitle="Get the latest updates"
title={t("footer.socials.twitter.title")}
subtitle={t("footer.socials.twitter.subtitle")}
href="https://twitter.com/AccordsLibrary"
/>
<LinkCard
icon="github-brands"
title="GitHub"
subtitle="Join the technical side"
title={t("footer.socials.github.title")}
subtitle={t("footer.socials.github.subtitle")}
href="https://github.com/Accords-Library"
/>
<LinkCard
icon="material-symbols:mail-outline"
title="Contact"
subtitle="Send us an email"
href="/contact"
title={t("footer.socials.contact.title")}
subtitle={t("footer.socials.contact.subtitle")}
href={getLocalizedUrl("/contact")}
/>
</div>
</section>

View File

@ -9,35 +9,19 @@ const { img, name, href } = Astro.props;
---
<a href={href} aria-label={name} class="keycap">
{img === undefined ? <p>{name}</p> : <div />}
{
img ? (
<>
<img src={img.light} class="when-light-theme" alt={name} title={name} />
<img src={img.dark} class="when-dark-theme" alt={name} title={name} />
</>
) : (
<p>{name}</p>
)
}
</a>
<style
define:vars={{
"light-image": `url(${img?.light})`,
"dark-image": `url(${img?.dark})`,
}}
>
:global(html):not(.manual-theme) {
a > div {
@media (prefers-color-scheme: light) {
background-image: var(--light-image);
}
@media (prefers-color-scheme: dark) {
background-image: var(--dark-image);
}
}
}
:global(html).light-theme a > div {
background-image: var(--light-image);
}
:global(html).dark-theme a > div {
background-image: var(--dark-image);
}
<style>
a {
font-size: 24px;
font-weight: 400;
@ -51,10 +35,8 @@ const { img, name, href } = Astro.props;
user-select: none;
& > div {
background-size: contain;
background-position: center;
background-repeat: no-repeat;
& > img {
object-fit: contain;
aspect-ratio: 2;
height: 100%;
width: 100%;

View File

@ -32,6 +32,7 @@ const { icon, subtitle, title, href } = Astro.props;
& > svg {
width: clamp(1.5em, 6vw + 0.8em, 3em);
height: clamp(1.5em, 6vw + 0.8em, 3em);
flex-shrink: 0;
}
& > #right {

View File

@ -1,3 +1,42 @@
{
"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>."
"global.siteName": "Accords 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. <strong>Beware there can be spoilers.</strong>",
"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.",
"header.topbar.search.tooltip": "Search on this website",
"header.topbar.theme.tooltip": "Switch between dark/light mode",
"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 websites 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>Accords 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>."
}

View File

@ -1,3 +1,11 @@
{
"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>."
"footer.links.home.title": "Accueil",
"footer.links.timeline.title": "Chronologie",
"footer.links.timeline.subtitle": "{{eraCount}} époques, {{eventCount}} évenements",
"footer.links.gallery.title": "Gallerie",
"footer.links.gallery.subtitle": "{{count}} images",
"footer.links.videos.title": "Vidéos",
"footer.links.videos.subtitle": "{{count}} vidéos",
"footer.links.webArchives.title": "Archives web",
"footer.links.webArchives.subtitle": "{{count}} archives"
}

View File

@ -1,3 +1,7 @@
import en from "./en.json";
type WordingKeys = keyof typeof en;
export const getI18n = async (locale: string) => {
const file = Bun.file(`./translations/${locale}.json`, {
type: "application/json",
@ -5,12 +9,126 @@ export const getI18n = async (locale: string) => {
const content = await file.text();
const translations: Record<string, string> = JSON.parse(content);
return {
t: (key: string): string => {
if (key in translations) {
return translations[key]!;
const formatWithValues = (
templateName: string,
template: string,
values: Record<string, any>
): string => {
Object.entries(values).forEach(([key, value]) => {
if (
!template.match(new RegExp(`{{ ${key}\\+|{{ ${key}\\?|{{ ${key} }}`))
) {
console.warn(
"Value",
key,
"has been provided but is not present in template",
templateName
);
return;
}
return `MISSING KEY: ${key}`;
if (typeof value === "number") {
// Find "plural" tokens
const matches = [
...template.matchAll(
new RegExp(`{{ ${key}\\+,[\\w\\s=>{},]+ }}`, "g")
),
].map(limitMatchToBalanceCurlyBraces);
const handlePlural = (match: string): string => {
match = match.substring(3, match.length - 3);
const options = match.split(",").splice(1);
for (const option of options) {
const optionCondition = option.split("{")[0];
if (!optionCondition) continue;
let optionValue = option.substring(optionCondition.length + 1);
if (!optionValue) continue;
optionValue = optionValue.substring(0, optionValue.length - 1);
if (option.startsWith("=")) {
const optionConditionValue = Number.parseInt(
optionCondition.substring(1)
);
if (value === optionConditionValue) {
return optionValue;
}
} else if (option.startsWith(">")) {
const optionConditionValue = Number.parseInt(
optionCondition.substring(1)
);
if (value > optionConditionValue) {
return optionValue;
}
} else if (option.startsWith("<")) {
const optionConditionValue = Number.parseInt(
optionCondition.substring(1)
);
if (value < optionConditionValue) {
return optionValue;
}
}
}
return "";
};
matches.forEach((match) => {
template = template.replace(match, handlePlural(match));
});
}
// Find "conditional" tokens
const matches = [
...template.matchAll(new RegExp(`{{ ${key}\\?,[\\w\\s{},]+ }}`, "g")),
].map(limitMatchToBalanceCurlyBraces);
const handleConditional = (match: string): string => {
match = match.substring(3, match.length - 3);
const options = match.split(",").splice(1);
if (value !== undefined && value !== null && value !== "") {
return options[0] ?? "";
}
return options[1] ?? "";
};
matches.forEach((match) => {
template = template.replace(match, handleConditional(match));
});
// Find "variable" tokens
let prettyValue = value;
if (typeof prettyValue === "number") {
prettyValue = prettyValue.toLocaleString(locale);
}
template = template.replaceAll(`{{ ${key} }}`, prettyValue);
});
return template;
};
return {
t: (key: WordingKeys, values: Record<string, any> = {}): string => {
if (key in translations) {
return formatWithValues(key, translations[key]!, values);
}
return `«${key}»`;
},
getLocalizedUrl: (url: string): string => `/${locale}${url}`,
};
};
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);
};