Added custom translation format
This commit is contained in:
parent
4408ac64d7
commit
f2e433c3f7
|
@ -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
143
README.md
|
@ -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`
|
|
@ -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>
|
|
@ -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">Accord’s 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 website’s 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>Accord’s Library</strong> is not affiliated with or endorsed by <strong
|
||||
>SQUARE ENIX CO. LTD</strong
|
||||
>. All game assets and promotional materials belongs to <strong
|
||||
>© SQUARE ENIX CO. LTD</strong
|
||||
>.
|
||||
</div>
|
||||
<div id="copyright" set:html={t("footer.disclaimer")} />
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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",
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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",
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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">Accord’s 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>
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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": "Accord’s Library",
|
||||
"global.siteSubtitle": "Discover • Archive • Translate • Analyze",
|
||||
"home.title": "Home",
|
||||
"home.description": "We aim at archiving and translating all of <strong>Yoko Taro</strong>’s works.<br />Yoko Taro is a Japanese video game director and scenario writer. He is best-known for his involvement with the <strong>NieR</strong > and <strong>Drakengard</strong> series. To complement his games, Yoko Taro likes to publish side materials in the form of books, anime, manga, audio books, novellas, even theater plays.<br />These media can be very difficult to find. His work goes all the way back to 2003. Most of it was released solely in Japanese, and sometimes in short supply. So this is what we do here: <strong>discover, archive, translate, and analyze</strong>.",
|
||||
"home.aboutUsButton": "Read more about us",
|
||||
"home.librarySection.title": "The Library",
|
||||
"home.librarySection.description": "Here you will find a list of IPs Yoko Taro worked on. Select one to discover all the media/content/articles that relates to this IP. <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 website’s content is made available under <a href=\"https://creativecommons.org/licenses/by-sa/4.0/\">CC-BY-SA</a> unless otherwise noted.",
|
||||
"footer.license.icons.tooltip": "CC-BY-SA 4.0 License",
|
||||
|
||||
"footer.disclaimer": "<strong>Accord’s Library</strong> is not affiliated with or endorsed by <strong>SQUARE ENIX CO. LTD</strong>. All game assets and promotional materials belongs to <strong>© SQUARE ENIX CO. LTD</strong>."
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue