Merge pull request #102 from Accords-Library/icu-i18n

Icu i18n
This commit is contained in:
DrMint 2023-01-31 23:41:21 +01:00 committed by GitHub
commit e3e67b8dbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1488 additions and 831 deletions

View File

@ -1,4 +1,5 @@
src/graphql/generated.ts
src/graphql/icuParams.ts
src/shared
.eslintrc.js
graphql-codegen.config.js

120
README.md
View File

@ -4,69 +4,128 @@
[![GitHub](https://img.shields.io/github/license/Accords-Library/accords-library.com?style=flat-square)](https://github.com/Accords-Library/accords-library.com/blob/main/LICENSE)
![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/Accords-Library/accords-library.com?style=flat-square)
## Introduction
Accords Library is a fan-site that aims at gathering and archiving all of Yoko Taros work.
Yoko Taro is a Japanese video game director and scenario writer. He is best-known for his work on the NieR and Drakengard (Drag-on Dragoon) franchises.
## Technologies
#### [Back](https://github.com/Accords-Library/strapi.accords-library.com)
### Overview
- CMS: Stapi
- GraphQL endpoint
- Multilanguage support
- Markdown format for the rich text fields
- Use webhooks to notify the front-end and image processor of updates
![](docs/project-mind-map.png)
#### [Image Processor](https://github.com/Accords-Library/img.accords-library.com)
_Purple connections are actions done at build-time only. Grey connections can be at build-time or run-time._
- Convert the images from the CMS to 4 formats
- Small: 512x512, quality 60, .webp
- Medium: 1024x1024, quality 75, .webp
- Large: 2048x2048, quality 80, .webp
- Og: 512x512, quality 60, .jpg
### [strapi.accords-library.com](https://github.com/Accords-Library/strapi.accords-library.com)
#### [Front](https://github.com/Accords-Library/accords-library.com) (this repository)
Our Content Management System (CMS) that uses [Strapi](https://strapi.io/).
- Use the official [GraphQL plugin](https://market.strapi.io/plugins/@strapi-plugin-graphql)
- Multilanguage support
- Markdown format for the rich text fields
- Use webhooks to notify the front-end, search engine, and image processor when new content/media has been created/modified/deleted
### [img.accords-library.com](https://github.com/Accords-Library/img.accords-library.com)
A custom made image processor to overcome the lack of customization offered by Strapi build-in image processor. There is a python script to bulk process all images uploaded to Strapi. Subsequent changes to Strapi's media library can be handled using webhooks. The repo includes a server that listen to these webhook calls, and another to serve the images.
Each image in Strapi's media library is converted to four different formats:
- Small: 512x512, quality 60, .webp
- Medium: 1024x1024, quality 75, .webp
- Large: 2048x2048, quality 80, .webp
- Og: 512x512, quality 60, .jpg
### [search.accords-library.com](https://github.com/Accords-Library/search.accords-library.com)
A search engine that uses [Meilisearch](https://www.meilisearch.com/).
The repo includes a docker-compose file to run an instance of Meilisearch. There is also a server that populates Meilisearch's documents at startup, then listen to webhooks sent by Strapi for subsequent changes.
### [gallery.accords-library.com](https://github.com/Accords-Library/gallery.accords-library.com)
An image board engine, uses [Szurubooru](https://github.com/rr-/szurubooru), a lighweight engine inspired by Danbooru (and Booru-type galleries in general). Unlike the other subdomains, this repo is completely separated from the rest of the stack.
### [watch.accords-library.com](https://github.com/Accords-Library/watch.accords-library.com)
A set of tools to archive videos on multiple platforms. The repo contains a CLI tool to archive YouTube videos. There is also a Python script which import the videos metadata to Strapi using GraphQL mutations. Finally, there's a server to serve the video files and thumbnail.
### [umami.accords-library.com](https://umami.is/)
An open-source self-hosted alternative to Google Analytics which doesn't require a cookie notice to be GDPR compliant.
### [accords-library.com](https://github.com/Accords-Library/accords-library.com) (this repository)
A detailled look at the technologies used in this repository:
- Language: [TypeScript](https://www.typescriptlang.org/)
- Framework: [Next.js 13](https://nextjs.org/) (React 18)
- SSG + ISR (Static Site Generation + Incremental Static Regeneration)
- The website is built before running in production
- Performances are great, and it's possible to deploy the app on a CDN
- On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted
- UI localizations are downloaded separetely into the `public/local-data` to avoid fetching the same static props for every pages.
- Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/)
- Fetch the GraphQL schema from the GraphQL back-end endpoint
- Read the operations and fragments stored as graphql files in the `src/graphql` folder
- Automatically generates a typesafe ready to use SDK using [graphql-request](https://www.npmjs.com/package/graphql-request) as the GraphQL client
- Markdown: [markdown-to-jsx](https://www.npmjs.com/package/markdown-to-jsx)
- Support for arbitrary React Components and Component Props!
- Autogenerated multi-level table of content and anchor links for the different headers
- Styling: [Tailwind CSS](https://tailwindcss.com/)
- Support for [Material Symbols](https://fonts.google.com/icons)
- Support for creating any arbitrary theming mode by swapping CSS variables
- Support for creating any arbitrary theme by swapping CSS variables
- Support for Container Queries (media queries at the element level)
- The website has a three-column layout, which turns into one-column + 2 toggleable side-menus if the screen is too narrow.
- Check out our [Design System Showcase](https://accords-library.com/dev/showcase/design-system)
- State Management: [Jōtai](https://jotai.org/)
- Jōtai is a small-weighted library for atomic state management
- Persistent app state using LocalStorage and SessionStorage
- Markdown
- Use [Marked](https://www.npmjs.com/package/marked) to convert markdown to HTML (which is then sanitized using [DOMPurify](https://www.npmjs.com/package/isomorphic-dompurify))
- Support for arbitrary React Components and Component Props using [markdown-to-jsx](https://www.npmjs.com/package/markdown-to-jsx)
- Autogenerated multi-level table of content and anchor links for the different headers
- Accessibility
- Gestures using [react-swipeable](https://www.npmjs.com/package/react-swipeable)
- Keyboard hotkeys using [react-hotkeys-hook](https://www.npmjs.com/package/react-hotkeys-hook)
- Support for light and dark mode with a manual switch and system's selected theme by default
- Fonts can be swaped to [OpenDyslexic](https://www.npmjs.com/package/@fontsource/opendyslexic)
- Multilingual
- By default, use the browser's language as the main language
- Fallback languages are used for content which are not available in the main language
- Main and fallback languages can be ordered manually by the user
- At the content level, the user can know which language is available
- Furthermore, the user can temporary select another language then the one that was automatically selected
- SSG + ISR (Static Site Generation + Incremental Static Regeneration)
- The website is built before running in production
- Performances are great, and possibility to deploy the app on a CDN
- On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted
- UI localizations are downloaded separetely into the `public/local-data` to avoid fetching the same static props for every page.
- UI Localizations
- The translated wordings use [ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/) to include variables, plural, dates...
- Use a custom ICU Typescript transformation script to provide type safety when formatting ICU wordings
- Fallback to English if a specific working isn't available in the user's language
- SEO
- Good defaults for the metadata and OpenGraph properties
- Each page can provide a custom thumbnail, title, description to be used
- Automatic generation of the sitemap using [next-sitemap](https://www.npmjs.com/package/next-sitemap)
- Data quality testing
- Data Quality Testing
- Data from the CMS is subject to a battery of tests (about 20 warning types and 40 error types) at build time
- Each warning/error comes with a front-end link to the incriminating element, as well as a link to the CMS to fix it
- Check for completeness, conformity, and integrity
- Code quality and style
- Code Quality and Style
- React Strict Mode
- [Eslint](https://www.npmjs.com/package/eslint) with [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import), [typescript-eslint](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin)
@ -75,7 +134,11 @@
- Other
- Custom book reader based on [Okuma-Reader](https://github.com/DrMint/Okuma-Reader)
- Support for [Material Symbols](https://fonts.google.com/icons)
- Custom lightbox using [react-zoom-pan-pinch](https://www.npmjs.com/package/react-zoom-pan-pinch)
- Handle query params using [Zod](https://zod.dev/)
- A secret "Terminal" mode. Can you find it?
## Installation
@ -91,9 +154,9 @@ Create a env file:
nano .env.local
```
Enter the followind information:
Enter the following information:
```txt
```
URL_GRAPHQL=https://url-to.strapi-accords-library.com/graphql
ACCESS_TOKEN=abcdef0123456789
REVALIDATION_TOKEN=abcdef0123456789
@ -104,7 +167,8 @@ NEXT_PUBLIC_URL_CMS=https://url-to.strapi-accords-library.com
NEXT_PUBLIC_URL_IMG=https://url-to.img-accords-library.com
NEXT_PUBLIC_URL_WATCH=https://url-to.watch-accords-library.com
NEXT_PUBLIC_URL_SELF=https://url-to-front-accords-library.com
NEXT_PUBLIC_URL_SEARCH=https://url-to.search-accords-library.com
NEXT_PUBLIC_URL_MEILISEARCH=https://url-to.search-accords-library.com
NEXT_PUBLIC_MEILISEARCH_KEY=abcdef0123456789
NEXT_PUBLIC_UMAMI_URL=https://url-to.umami-accords-library.com
NEXT_PUBLIC_UMAMI_ID=abcdef0123456789
```

View File

@ -0,0 +1,134 @@
<?xml version="1.0"?>
<minder version="1.14.0" parent-etag="2844169042" etag="3777682473">
<theme name="dark" label="Dark" index="-1"/>
<styles>
<style level="0" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="rounded" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="10" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<style level="1" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<style level="2" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<style level="3" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<style level="4" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<style level="5" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<style level="6" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<style level="7" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<style level="8" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<style level="9" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<style level="10" isset="true" branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true" connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
</styles>
<drawarea x="-736.36765543619742" y="32.05864461263036" scale="1.5"/>
<images/>
<nodes>
<node id="0" posx="648.76816813151015" posy="501.13512166341127" width="181" height="56" side="left" fold="false" treesize="56" layout="Horizontal" group="false">
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
<nodename posx="667.76816813151015" posy="520.13512166341127" maxwidth="200">
<text data="accords-library.com"/>
</nodename>
<nodenote></nodenote>
</node>
<node id="1" posx="603.1911417643222" posy="198.59891764322904" width="228" height="56" side="right" fold="false" treesize="56" layout="Horizontal" group="false">
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
<nodename posx="622.1911417643222" posy="217.59891764322904" maxwidth="200">
<text data="strapi.accords-library.com"/>
</nodename>
<nodenote></nodenote>
</node>
<node id="2" posx="508.77593994140574" posy="358.0493469238279" width="230" height="56" side="right" fold="false" treesize="56" layout="Horizontal" group="false">
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
<nodename posx="527.7759399414058" posy="377.0493469238279" maxwidth="200">
<text data="watch.accords-library.com"/>
</nodename>
<nodenote></nodenote>
</node>
<node id="3" posx="959.78491210937489" posy="490.81981404622388" width="235" height="56" side="left" fold="false" treesize="56" layout="Horizontal" group="false">
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
<nodename posx="978.78491210937489" posy="509.81981404622388" maxwidth="200">
<text data="search.accords-library.com"/>
</nodename>
<nodenote></nodenote>
</node>
<node id="4" posx="300.46666463216098" posy="474.56320190429688" width="213" height="56" side="left" fold="false" treesize="56" layout="Horizontal" group="false">
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
<nodename posx="319.46666463216098" posy="493.56320190429688" maxwidth="200">
<text data="img.accords-library.com"/>
</nodename>
<nodenote></nodenote>
</node>
<node id="5" posx="753.05198160807242" posy="709.79446411132812" width="236" height="56" side="right" fold="false" treesize="56" layout="Horizontal" group="false">
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
<nodename posx="772.05198160807242" posy="728.79446411132812" maxwidth="200">
<text data="umami.accords-library.com"/>
</nodename>
<nodenote></nodenote>
</node>
<node id="6" posx="468.00632731119703" posy="709.85610961914062" width="234" height="56" side="right" fold="false" treesize="56" layout="Horizontal" group="false">
<style branchmargin="100" branchradius="25" linktype="straight" linkwidth="4" linkarrow="false" linkdash="solid" nodeborder="underlined" nodewidth="200" nodeborderwidth="4" nodefill="false" nodemargin="8" nodepadding="11" nodefont="Sans 11" nodemarkup="true"/>
<nodename posx="487.00632731119703" posy="728.85610961914062" maxwidth="200">
<text data="gallery.accords-library.com"/>
</nodename>
<nodenote></nodenote>
</node>
</nodes>
<groups/>
<connections>
<connection from_id="3" to_id="1" drag_x="1038.6269124348951" drag_y="296.62844848632801" color="#813d9c">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<title>GraphQL queries</title>
<note></note>
</connection>
<connection from_id="1" to_id="3" drag_x="982.0184326171875" drag_y="347.55404663085926">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<title>Webhook</title>
<note></note>
</connection>
<connection from_id="2" to_id="1" drag_x="640.25118001302098" drag_y="308.37991333007801" color="#813d9c">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<title>GraphQL mutations</title>
<note></note>
</connection>
<connection from_id="0" to_id="5" drag_x="801.02655029296898" drag_y="644.59735107421943">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<title>Sends events</title>
<note></note>
</connection>
<connection from_id="4" to_id="0" drag_x="531.34985351562477" drag_y="582.42803955078148">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="150"/>
<title>Provides the images</title>
<note></note>
</connection>
<connection from_id="3" to_id="0" drag_x="917.41172281901027" drag_y="585.41107177734443">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="150"/>
<title>Provides search results</title>
<note></note>
</connection>
<connection from_id="0" to_id="1" drag_x="872.998291015625" drag_y="408.14896647135413">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<title>GraphQL queries</title>
<note></note>
</connection>
<connection from_id="0" to_id="6" drag_x="664.23213704427053" drag_y="645.67006429036542">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<title>Links to</title>
<note></note>
</connection>
<connection from_id="4" to_id="1" drag_x="400.83854166666663" drag_y="295.4715677897135" color="#813d9c">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<title>Python script</title>
<note></note>
</connection>
<connection from_id="2" to_id="0" drag_x="645.96777343749955" drag_y="448.91068522135413">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="150"/>
<title>Provides the videos</title>
<note></note>
</connection>
<connection from_id="1" to_id="4" drag_x="441.14660644531227" drag_y="342.38249715169252">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<title>Webhook</title>
<note></note>
</connection>
<connection from_id="1" to_id="0" drag_x="790.7373046875" drag_y="369.4803466796875">
<style connectiondash="dotted" connectionlwidth="2" connectionarrow="fromto" connectionpadding="3" connectionfont="Sans 9" connectiontwidth="100"/>
<title>Webhook</title>
<note></note>
</connection>
</connections>
<stickers/>
</minder>

BIN
docs/project-mind-map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

186
package-lock.json generated
View File

@ -10,9 +10,11 @@
"@fontsource/share-tech-mono": "^4.5.9",
"@fontsource/vollkorn": "^4.5.14",
"@fontsource/zen-maru-gothic": "^4.5.16",
"@formatjs/icu-messageformat-parser": "^2.1.14",
"@tippyjs/react": "^4.2.6",
"autoprefixer": "^10.4.13",
"cuid": "^2.1.8",
"intl-messageformat": "^10.2.5",
"isomorphic-dompurify": "^0.26.0",
"jotai": "^1.13.1",
"markdown-to-jsx": "^7.1.8",
@ -22,7 +24,7 @@
"next": "^13.1.5",
"nodemailer": "^6.9.0",
"rc-slider": "^10.1.0",
"react": "18.2.0",
"react": "^18.2.0",
"react-dom": "18.2.0",
"react-hotkeys-hook": "^3.4.7",
"react-swipeable": "^7.0.0",
@ -1529,6 +1531,75 @@
"resolved": "https://registry.npmjs.org/@fontsource/zen-maru-gothic/-/zen-maru-gothic-4.5.16.tgz",
"integrity": "sha512-KM3z1IfKRF3p9nE2TEyVSbQUHhrpSh14cUZ8B6asjOzgzS8PXXyUDD+vmvptIswI3C/1tMQUBns8mzuub3lHZQ=="
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz",
"integrity": "sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==",
"dependencies": {
"@formatjs/intl-localematcher": "0.2.32",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/ecma402-abstract/node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/@formatjs/fast-memoize": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.7.tgz",
"integrity": "sha512-hPeM5LXUUjtCKPybWOUAWpv8lpja8Xz+uKprFPJcg5F2Rd+/bf1E0UUsLRpaAgOReAf5HMRtoIgv/UcyPICrTQ==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/fast-memoize/node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.14.tgz",
"integrity": "sha512-0KqeVOb72losEhUW+59vhZGGd14s1f35uThfEMVKZHKLEObvJdFTiI3ZQwvTMUCzLEMxnS6mtnYPmG4mTvwd3Q==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/icu-skeleton-parser": "1.3.18",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser/node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.3.18",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz",
"integrity": "sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.14.3",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser/node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.2.32",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
"integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/intl-localematcher/node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/@graphql-codegen/cli": {
"version": "2.16.4",
"resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.16.4.tgz",
@ -6962,6 +7033,22 @@
"node": ">= 0.4"
}
},
"node_modules/intl-messageformat": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.2.5.tgz",
"integrity": "sha512-AievYMN6WLLHwBeCTv4aRKG+w3ZNyZtkObwgsKk3Q7GNTq8zDRvDbJSBQkb2OPeVCcAKcIXvak9FF/bRNavoww==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/fast-memoize": "1.2.7",
"@formatjs/icu-messageformat-parser": "2.1.14",
"tslib": "^2.4.0"
}
},
"node_modules/intl-messageformat/node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -11662,6 +11749,85 @@
"resolved": "https://registry.npmjs.org/@fontsource/zen-maru-gothic/-/zen-maru-gothic-4.5.16.tgz",
"integrity": "sha512-KM3z1IfKRF3p9nE2TEyVSbQUHhrpSh14cUZ8B6asjOzgzS8PXXyUDD+vmvptIswI3C/1tMQUBns8mzuub3lHZQ=="
},
"@formatjs/ecma402-abstract": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz",
"integrity": "sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==",
"requires": {
"@formatjs/intl-localematcher": "0.2.32",
"tslib": "^2.4.0"
},
"dependencies": {
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
}
}
},
"@formatjs/fast-memoize": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.7.tgz",
"integrity": "sha512-hPeM5LXUUjtCKPybWOUAWpv8lpja8Xz+uKprFPJcg5F2Rd+/bf1E0UUsLRpaAgOReAf5HMRtoIgv/UcyPICrTQ==",
"requires": {
"tslib": "^2.4.0"
},
"dependencies": {
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
}
}
},
"@formatjs/icu-messageformat-parser": {
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.14.tgz",
"integrity": "sha512-0KqeVOb72losEhUW+59vhZGGd14s1f35uThfEMVKZHKLEObvJdFTiI3ZQwvTMUCzLEMxnS6mtnYPmG4mTvwd3Q==",
"requires": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/icu-skeleton-parser": "1.3.18",
"tslib": "^2.4.0"
},
"dependencies": {
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
}
}
},
"@formatjs/icu-skeleton-parser": {
"version": "1.3.18",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz",
"integrity": "sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg==",
"requires": {
"@formatjs/ecma402-abstract": "1.14.3",
"tslib": "^2.4.0"
},
"dependencies": {
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
}
}
},
"@formatjs/intl-localematcher": {
"version": "0.2.32",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
"integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
"requires": {
"tslib": "^2.4.0"
},
"dependencies": {
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
}
}
},
"@graphql-codegen/cli": {
"version": "2.16.4",
"resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.16.4.tgz",
@ -15915,6 +16081,24 @@
"side-channel": "^1.0.4"
}
},
"intl-messageformat": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.2.5.tgz",
"integrity": "sha512-AievYMN6WLLHwBeCTv4aRKG+w3ZNyZtkObwgsKk3Q7GNTq8zDRvDbJSBQkb2OPeVCcAKcIXvak9FF/bRNavoww==",
"requires": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/fast-memoize": "1.2.7",
"@formatjs/icu-messageformat-parser": "2.1.14",
"tslib": "^2.4.0"
},
"dependencies": {
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
}
}
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",

View File

@ -3,10 +3,11 @@
"private": true,
"scripts": {
"dev": "next dev -p 12499",
"precommit": "npm run fetch-local-data && npm run prettier && npm run unused-exports && npm run eslint && npm run tsc && echo ALL PRECOMMIT CHECKS PASSED SUCCESSFULLY, LET\\'S FUCKING GO!",
"precommit": "npm run fetch-local-data && npm run icu-to-ts && npm run unused-exports && npm run prettier && npm run eslint && npm run tsc && echo ALL PRECOMMIT CHECKS PASSED SUCCESSFULLY, LET\\'S FUCKING GO!",
"unused-exports": "ts-unused-exports ./tsconfig.json --excludePathsFromReport=src/pages --ignoreFiles=generated",
"fetch-local-data": "npm run generate && esrun src/graphql/fetchLocalData.ts --esrun",
"prebuild": "npm run fetch-local-data",
"icu-to-ts": "esrun src/graphql/icuToTypescript.ts --icu",
"prebuild": "npm run fetch-local-data && npm run icu-to-ts",
"build": "next build",
"postbuild": "next-sitemap --config next-sitemap.config.js",
"start": "next start -p 12500",
@ -21,9 +22,11 @@
"@fontsource/share-tech-mono": "^4.5.9",
"@fontsource/vollkorn": "^4.5.14",
"@fontsource/zen-maru-gothic": "^4.5.16",
"@formatjs/icu-messageformat-parser": "^2.1.14",
"@tippyjs/react": "^4.2.6",
"autoprefixer": "^10.4.13",
"cuid": "^2.1.8",
"intl-messageformat": "^10.2.5",
"isomorphic-dompurify": "^0.26.0",
"jotai": "^1.13.1",
"markdown-to-jsx": "^7.1.8",
@ -33,7 +36,7 @@
"next": "^13.1.5",
"nodemailer": "^6.9.0",
"rc-slider": "^10.1.0",
"react": "18.2.0",
"react": "^18.2.0",
"react-dom": "18.2.0",
"react-hotkeys-hook": "^3.4.7",
"react-swipeable": "^7.0.0",

View File

@ -13,16 +13,14 @@
"wiki_short_description": "An encyclopedia for everything related to DrakeNieR",
"chronicles_short_description": "Experience all events and content in chronological order",
"news": "News",
"merch": "Merch",
"gallery": "Gallery",
"archives": "Archives",
"about_us": "About us",
"licensing_notice": "This websites content is made available under [CC-BY-SA](https://creativecommons.org/licenses/by-sa/4.0/) unless otherwise noted.",
"copyright_notice": "Accords Library is not affiliated with or endorsed by SQUARE ENIX CO. LTD. All game assets and promotional materials belongs to © SQUARE ENIX CO. LTD.",
"contents_description": "All the contents (textual, audio, and video) from the Library or other online sources.",
"type": "Type",
"category": "Category",
"categories": "Categories",
"type": "{ count, plural, =0 {No types} one {Type} other {Types} }",
"category": "{ count, plural, =0 {No categories} one {Category} other {Categories} }",
"size": "Size",
"release_date": "Release date",
"release_year": "Release year",
@ -31,23 +29,17 @@
"width": "Width",
"height": "Height",
"thickness": "Thickness",
"subitem": "Subitem",
"subitems": "Subitems",
"subitem_of": "Subitem of",
"variant": "Variant",
"variants": "Variants",
"variant_of": "Variant of",
"subitem": "{ count, plural, =0 {No subitems} one {Subitem} other {Subitems} }",
"variant": "{ count, plural, =0 {No variants} one {Variant} other {Variants} }",
"summary": "Summary",
"audio": "Audio",
"video": "Video",
"textual": "Textual",
"game": "Game",
"other": "Other",
"return_to": "Return to",
"left_to_right": "Left to right",
"right_to_left": "Right to left",
"page": "Page",
"pages": "Pages",
"page": "{ count, plural, =0 {No pages} one {Page} other {Pages} }",
"page_order": "Page order",
"binding": "Binding",
"type_information": "Type information",
@ -60,15 +52,12 @@
"view_scans": "View scans",
"paperback": "Paperback",
"hardcover": "Hardcover",
"languages": "Languages",
"select_language": "Select a language",
"language": "Language",
"language": "{ count, plural, =0 {No languages} one {Language} other {Languages} }",
"library_description": "A comprehensive list of all Yokoverses side materials (books, novellas, artbooks, stage plays, manga, drama CDs, and comics). For each, we provide photos, scans, and transcript of the content, information about what it is, when and how it was released, size, initial price…",
"wiki_description": "An encyclopedia for everything related to DrakeNieR. Right now, we only have the Chronology but a lot more pages are planned to be released!",
"chronicles_description": "Experience all events and content in chronological order.",
"news_description": "News articles written by our Recorders! Here you will find announcements about new merch/items releases, guides, theories, unboxings, showcases...",
"merch_description": "Harum ut consequatur a earum explicabo suscipit. Nostrum asperiores consectetur aperiam in ut sunt. Ipsa quibusdam et vel quam voluptas placeat. Qui est aliquam voluptatem. Tempora nisi exercitationem tempore sapiente expedita. Voluptas ut eaque nulla sunt ut dolor corrupti quos.",
"gallery_description": "A fully tagged Danbooru-styled gallery with currently more than a thousand unique official artworks.",
"archives_description": "Besides physical medias, we also archive digital contents such as websites, webpages, videos, and documents.",
"about_us_description": "Find more information about the Accord's Library project in the following pages.",
"page_not_found": "Oops! Were having trouble finding this page",
@ -77,8 +66,6 @@
"show_subitems": "Show subitems",
"show_primary_items": "Show primary items",
"show_secondary_items": "Show secondary items",
"no_type": "No type",
"no_year": "No year",
"order_by": "Order by",
"group_by": "Group by",
"select_option_sidebar": "Select one of the options in the sidebar",
@ -109,20 +96,14 @@
"translation_notice": "This content is a fan-translation",
"source_language": "Source language",
"pronouns": "Pronouns",
"no_category": "No category",
"item": "Item",
"items": "Items",
"item": "{ count, plural, =0 {No items} one {Item} other {Items} }",
"content": "Content",
"result": "Result",
"results": "Results",
"language_switch_message": "",
"open_settings": "Open settings",
"change_language": "Change language",
"open_search": "Open search",
"chronology": "Chronology",
"accords_handbook": "Accord's Handbook",
"legality": "Legality",
"members": "Members",
"sharing_policy": "Sharing Policy",
"contact_us": "Contact us",
"email": "Email",
@ -155,7 +136,6 @@
"only_display_unmarked_items": "Only display unmarked items",
"display_all_items": "Display all items",
"table_of_contents": "Table of Contents",
"definition": "Definition",
"no_results_message": "No results. You can try changing or resetting the search parameters.",
"all": "All",
"special_pages": "Special Pages",
@ -193,7 +173,13 @@
"most_popular": "Most popular",
"shortest": "Shortest",
"longest": "Longest",
"search": "Search"
"search": "Search",
"showing_x_out_of_y_results": "Showing {x} out of {y} results",
"return_to_x": "Return { x, select, undefined {} other {to {x}} }",
"x_results": "{ x, plural, =0 {No results} one {# result} other {# results} }",
"definition_x": "Definition {x}",
"subitem_of_x": "Subitem of {x}",
"variant_of_x": "Variant of {x}"
}
},
{
@ -208,16 +194,14 @@
"wiki_short_description": "Une encyclopédie pour tout l'univers DrakeNieR",
"chronicles_short_description": "Parcourir tous les événements et les contenu dans l'ordre chronologique",
"news": "News",
"merch": "Merch",
"gallery": "Galerie",
"archives": "Archives",
"about_us": "À propos",
"licensing_notice": "Le contenu de ce site web est mis à disposition sous licence [CC-BY-SA](https://creativecommons.org/licenses/by-sa/4.0/), sauf indication contraire.",
"copyright_notice": "Accord's Library n'est pas affiliée ni approuvée par SQUARE ENIX CO. LTD. Tous les contenus du jeu et les contenus promotionnel appartiennent à © SQUARE ENIX CO. LTD.",
"contents_description": "",
"type": "Type",
"category": "Catégorie",
"categories": "Catégories",
"contents_description": "Tous les contenus (textuels, audio et vidéo) de la Bibliothèque ou d'autres sources en ligne.",
"type": "{ count, plural, =0 {Pas de type} one {Type} other {Types} }",
"category": "{ count, plural, =0 {Pas de catégorie} one {Catégorie} other {Catégories} }",
"size": "Dimension",
"release_date": "Date de sortie",
"release_year": "Année de sortie",
@ -226,23 +210,17 @@
"width": "Largeur",
"height": "Hauteur",
"thickness": "Épaisseur",
"subitem": "Sous-item",
"subitems": "Sous-items",
"subitem_of": "Sous-item de",
"variant": "Variante",
"variants": "Variantes",
"variant_of": "Variante de",
"subitem": "{ count, plural, =0 {Pas de sous-item} one {Sous-item} other {Sous-items} }",
"variant": "{ count, plural, =0 {Pas de variante} one {Variante} other {Variantes} }",
"summary": "Résumé",
"audio": "Audio",
"video": "Vidéo",
"textual": "Textuel",
"game": "Jeux",
"other": "Autre",
"return_to": "Retourner à ",
"left_to_right": "Gauche à droite",
"right_to_left": "Droite à gauche",
"page": "Page",
"pages": "Pages",
"page": "{ count, plural, =0 {Aucune page} one {Page} other {Pages} }",
"page_order": "Order des pages",
"binding": "Brochure",
"type_information": "Information de type",
@ -255,25 +233,20 @@
"view_scans": "Voir les scans",
"paperback": "Broché",
"hardcover": "Relié",
"languages": "Langues",
"select_language": "Séléctionner la langue",
"language": "Langue",
"library_description": "",
"wiki_description": "",
"chronicles_description": "",
"language": "{ count, plural, =0 {Aucune langue} one {Langue} other {Langues} }",
"library_description": "Une liste complète de tous les médias annexes du Yokoverse (livres, romans, artbooks, pièces de théâtre, mangas, CD de théâtre et bandes dessinées). Pour chacun, nous fournissons des photos, des scans et une transcription du contenu, des informations sur sa nature, quand il est sorti, sa taille, son prix initial...",
"wiki_description": "Une encyclopédie pour tout ce qui concerne DrakeNieR. Pour l'instant, nous n'avons que la Chronologie mais beaucoup plus de pages sont prévues !",
"chronicles_description": "Découvrez tous les événements et contenus de manière chronologique.",
"news_description": "Articles d'actualité écrits par nos Recorders ! Vous trouverez ici des annonces sur les nouvelles sorties de merch/items, des guides, des théories, des unboxings, des showcases...",
"merch_description": "Lorem ipsum",
"gallery_description": "",
"archives_description": "",
"about_us_description": "",
"archives_description": "Outre les supports physiques, nous archivons également les contenus numériques tels que les sites web, les pages web, les vidéos et les documents.",
"about_us_description": "Vous trouverez plus d'informations sur le projet Accord's Library dans les pages suivantes.",
"page_not_found": "Page introuvable",
"default_description": "Accord's Library a pour but de rassembler et d'archiver l'ensemble des travaux de Yoko Taro. Yoko Taro est un réalisateur et scénariste de jeux vidéo japonais.",
"name": "Nom",
"show_subitems": "Afficher les sous-items",
"show_primary_items": "Afficher les items primaires",
"show_secondary_items": "Afficher les items secondaires",
"no_type": "Pas de type",
"no_year": "Pas d'année",
"order_by": "Ordonné par",
"group_by": "Groupé par",
"select_option_sidebar": "Sélectionner l'une des options de la barre latérale",
@ -287,7 +260,7 @@
"player_name": "Nom du joueur",
"currency": "Devise",
"font": "Police d'écriture",
"calculated": "calculé",
"calculated": "Calculé",
"status_incomplete": "Cette entrée n'est que partiellement traduite/transcrite.",
"status_draft": "Cette entrée n'est qu'un brouillon. Cela signifie généralement qu'il s'agit d'un travail en cours. La traduction/transcription peut être médiocre et/ou auto-générée par ordinateur.",
"status_review": "Cet entrée n'a pas encore été relue. Le contenu devrait néanmoins être correct.",
@ -304,20 +277,14 @@
"translation_notice": "Ceci est une traduction",
"source_language": "Langue source",
"pronouns": "Pronoms",
"no_category": "Pas de categorie",
"item": "Item",
"items": "Items",
"item": "{ count, plural, =0 {Aucun item} one {Item} other {Items} }",
"content": "Content",
"result": "Resultat",
"results": "Résultats",
"language_switch_message": "Ce contenu n'est pas disponible dans la langue actuellement sélectionnée. Vous pouvez sélectionner l'une des langues suivantes à la place :",
"open_settings": "Ouvrir les paramètres",
"change_language": "Changer de langue",
"open_search": "Ouvrir le menu de recherche",
"chronology": "Chronologie",
"accords_handbook": "Le manuel de Accord",
"legality": "Légalité",
"members": "Membres",
"sharing_policy": "Politique de partage",
"contact_us": "Nous contacter",
"email": "Email",
@ -350,7 +317,6 @@
"only_display_unmarked_items": "Seulement afficher les items non-marqués",
"display_all_items": "Afficher tous les items",
"table_of_contents": "Sommaire",
"definition": "Definition",
"no_results_message": "Aucun résultat. Vous pouvez essayer de modifier ou de réinitialiser les paramètres de recherche.",
"all": "Tous",
"special_pages": "Pages spéciales",
@ -388,7 +354,13 @@
"most_popular": "Moins populaires",
"shortest": "Plus courtes",
"longest": "Plus longues",
"search": "Rechercher"
"search": "Rechercher",
"showing_x_out_of_y_results": "{x} résultats sur {y} affichés",
"return_to_x": "Retour { x, select, undefined {} other {à {x}} }",
"x_results": "{ x, plural, =0 {Pas de résultat} one {# résultat} other {# résultats} }",
"definition_x": "Définition {x}",
"subitem_of_x": "Sous-item de {x}",
"variant_of_x": "Variante de {x}"
}
},
{
@ -403,7 +375,6 @@
"wiki_short_description": "ゲーム宇宙に関連するすべての百科事典です。",
"chronicles_short_description": "すべてのイベントとコンテンツを時系列で体験できる",
"news": "ニュース",
"merch": "マーチ",
"gallery": "ギャラリー",
"archives": "アーカイブス",
"about_us": "会社概要",
@ -412,7 +383,6 @@
"contents_description": "図書館や他のオンラインソースのすべてのコンテンツ(テキスト、オーディオ、ビデオ)。",
"type": "タイプ",
"category": "カテゴリー",
"categories": "カテゴリー",
"size": "サイズ",
"release_date": "発売日",
"release_year": "発売年",
@ -422,22 +392,16 @@
"height": "高さ",
"thickness": "厚み",
"subitem": "サブアイテム",
"subitems": "サブアイテム",
"subitem_of": "のサブアイテム",
"variant": "バリアント",
"variants": "バリアント",
"variant_of": "のバリアント",
"summary": "概要",
"audio": "オーディオ",
"video": "ビデオ",
"textual": "テキスト",
"game": "ゲーム",
"other": "他",
"return_to": "戻る",
"left_to_right": "左から右へ",
"right_to_left": "右から左へ",
"page": "ページ",
"pages": "ページ",
"page_order": "ページ順序",
"binding": "製本",
"type_information": "タイプ情報",
@ -450,15 +414,12 @@
"view_scans": "スキャンを開放",
"paperback": "ペーパーバック",
"hardcover": "ハードカバー",
"languages": "言語",
"select_language": "言語を選択する",
"language": "言語",
"library_description": "ヨコベースの副教材書籍、小説、画集、舞台劇、漫画、ドラマCD、コミックを網羅したリストです。それぞれについて、写真、スキャン、内容の書き起こし、どんなものなのか、いつ、どのように発売されたのか、サイズ、初回価格...などの情報を掲載しています。",
"wiki_description": "DrakeNieRに関連するすべての百科事典です。現在は年表のみですが、今後多くのページを公開予定です",
"chronicles_description": "Accord's Libraryは、ヨーコ・タローの全作品を収集・保存することを目的としています。ヨーコ・タローは、日本のゲームディレクター、シナリオライターです。",
"news_description": "レコーダーが書いたニュース記事です ここでは、新しい商品/アイテムのリリースに関するお知らせ、ガイド、セオリー、アンボックス、ショーケース...をご紹介しています。",
"merch_description": "",
"gallery_description": "",
"archives_description": "",
"about_us_description": "Accord's Libraryプロジェクトについては、以下のページで詳しくご紹介しています。",
"page_not_found": "ページが見つかりません",
@ -467,8 +428,6 @@
"show_subitems": "サブアイテムをみせる",
"show_primary_items": "一次のイテムをみせる",
"show_secondary_items": "二次のイテムをみせる",
"no_type": "タイプなし",
"no_year": "年なし",
"order_by": "注文する",
"group_by": "グループ化する",
"select_option_sidebar": "サイドバーのオプションを選択します",
@ -499,20 +458,14 @@
"translation_notice": "このコンテンツはファンによる翻訳です",
"source_language": "ソース言語",
"pronouns": "代名詞",
"no_category": "カテゴリーなし",
"item": "項目",
"items": "項目",
"content": "コンテンツ",
"result": "結果",
"results": "結果",
"language_switch_message": null,
"open_settings": "オープン設定",
"change_language": "言語を変更する",
"open_search": "オープンサーチ",
"chronology": "年表",
"accords_handbook": "アコードの手引き",
"legality": "合法性",
"members": "メンバー紹介",
"sharing_policy": "共有ポリシー",
"contact_us": "お問い合わせ",
"email": "電子メール",
@ -545,7 +498,6 @@
"only_display_unmarked_items": "無印のアイテムのみ表示",
"display_all_items": "すべての項目を表示する",
"table_of_contents": "目次",
"definition": "定義",
"no_results_message": "結果が出ません。検索条件を変更またはリセットしてみてください。",
"all": "すべて",
"special_pages": "特設ページ",
@ -583,7 +535,13 @@
"most_popular": null,
"shortest": null,
"longest": null,
"search": null
"search": null,
"showing_x_out_of_y_results": null,
"return_to_x": null,
"x_results": null,
"definition_x": null,
"subitem_of_x": null,
"variant_of_x": null
}
},
{
@ -598,7 +556,6 @@
"wiki_short_description": "Una enciclopedia para todo lo relacionado con DrakeNieR",
"chronicles_short_description": "Experimenta todos los eventos y contenidos en orden cronológico",
"news": "Novedades",
"merch": "Merch",
"gallery": "Galería",
"archives": "Archivos",
"about_us": "Sobre nosotros",
@ -607,7 +564,6 @@
"contents_description": "Todo el contenido (textual, audio y video) de la Biblioteca u otras fuentes en línea.",
"type": "Tipo",
"category": "Categoría",
"categories": "Categorías",
"size": "Tamaño",
"release_date": "Fecha de lanzamiento",
"release_year": "Año de lanzamiento",
@ -617,22 +573,16 @@
"height": "Altura",
"thickness": "Grosor",
"subitem": "Sub-item",
"subitems": "Sub-items",
"subitem_of": "Sub-item de",
"variant": "Variante",
"variants": "Variantes",
"variant_of": "Variante de",
"summary": "Sumario",
"audio": "Audio",
"video": "Video",
"textual": "Textual",
"game": "Juego",
"other": "Otros",
"return_to": "Volver a",
"left_to_right": "Izquierda a derecha",
"right_to_left": "Derecha a izquierda",
"page": "Página",
"pages": "Páginas",
"page_order": "Orden de las páginas",
"binding": "Encuadernación",
"type_information": "Tipo de información",
@ -645,15 +595,12 @@
"view_scans": "Ver escaneos",
"paperback": "Tapa blanda",
"hardcover": "Tapa dura",
"languages": "Idiomas",
"select_language": "Seleccionar idioma",
"language": "Idioma",
"library_description": "Una lista completa de todos los materiales complementarios de Yokoverse (libros, novelas, libros de arte, obras de teatro, manga, CDs novelizados y cómics). Para cada uno, proporcionamos fotos, escaneos y transcripciones del contenido, información sobre qué es, cuándo y cómo se ha publicado, tamaño, precio inicial...",
"wiki_description": "Una enciclopedia para todo lo relacionado con DrakeNieR. En este momento, solo tenemos la Cronología, ¡pero muchas más páginas están planeadas para ser publicadas!",
"chronicles_description": "",
"news_description": "¡Nuevos artículos escritos por nuestros/as Archivistas! Aquí encontrarás anuncios sobre nuevos lanzamientos de merchandising/artículos, guías, teorías, unboxings, showcases...",
"merch_description": "",
"gallery_description": "Una galería completamente etiquetada de estilo Danbooru, actualmente con más de mil obras de arte oficiales únicas.",
"archives_description": "",
"about_us_description": "Encuentra más información sobre el proyecto de Accord's Library en las siguientes páginas.",
"page_not_found": "Página no encontrada",
@ -662,8 +609,6 @@
"show_subitems": "Mostrar sub-items",
"show_primary_items": "Mostrar items principales",
"show_secondary_items": "Mostrar items secundarios",
"no_type": "Ningún tipo",
"no_year": "Ningún año",
"order_by": "Ordenar por",
"group_by": "Agrupar por",
"select_option_sidebar": "Selecciona una de las opciones en la barra lateral",
@ -671,7 +616,7 @@
"settings": "Ajustes",
"theme": "Tema",
"light": "Claro",
"auto": "Auto",
"auto": "Automático",
"dark": "Oscuro",
"font_size": "Tamaño de la fuente",
"player_name": "Nombre del jugador/a",
@ -694,83 +639,76 @@
"translation_notice": "Este contenido es una traducción de fans",
"source_language": "Idioma original",
"pronouns": "Pronombres",
"no_category": "Ningún categoría",
"item": null,
"items": null,
"content": null,
"result": null,
"results": null,
"language_switch_message": null,
"open_settings": null,
"change_language": null,
"open_search": null,
"chronology": null,
"accords_handbook": null,
"legality": null,
"members": null,
"sharing_policy": null,
"contact_us": null,
"item": "Ítem",
"content": "Contenido",
"open_settings": "Abrir ajustes",
"change_language": "Cambiar idioma",
"open_search": "Abrir búsqueda",
"chronology": "Cronología",
"accords_handbook": "Manual de Accord",
"legality": "Legalidad",
"sharing_policy": "Política de Uso Compartido",
"contact_us": "Contáctanos",
"email": "Email",
"email_gdpr_notice": "Solo usamos tu correo electrónico exclusivamente para contactarte en relación a tu solicitud. No compartimos este correo electrónico con nadie ni lo usamos para ningún otro propósito.",
"message": null,
"send": null,
"message": "Mensaje",
"send": "Enviar",
"response_invalid_code": "El código de verificación es incorrecto.",
"response_invalid_email": "¡Por favor, introduce una dirección de correo electrónico válida!",
"response_email_success": "¡Gracias por contactarnos! Nos pondremos en contacto contigo en breve.",
"always_show_info": null,
"item_not_available": null,
"primary_language": null,
"secondary_language": null,
"combine_related_contents": null,
"previous_content": null,
"followup_content": null,
"videos": null,
"always_show_info": "Siempre mostrar la información",
"item_not_available": "Ítem no disponible",
"primary_language": "Idioma primario",
"secondary_language": "Idioma secundario",
"combine_related_contents": "Combinar contenido relacionado",
"previous_content": "Contenido anterior",
"followup_content": "Contenido siguiente",
"videos": "Vídeos",
"view_on": null,
"channel": null,
"subscribers": null,
"description": null,
"available_at": null,
"search_title": null,
"want_it": null,
"have_it": null,
"source": null,
"reset_all_filters": null,
"only_display_items_i_have": null,
"only_display_items_i_want": null,
"only_display_unmarked_items": null,
"display_all_items": null,
"table_of_contents": null,
"definition": null,
"no_results_message": null,
"all": null,
"special_pages": null,
"scan": null,
"scanlation": null,
"scanners": null,
"cleaners": null,
"typesetters": null,
"notes": null,
"cover": null,
"tags": null,
"no_source_warning": null,
"copy_anchor_link": null,
"anchor_link_copied": null,
"folders": null,
"empty_folder_message": null,
"switch_to_grid_view": null,
"switch_to_folder_view": null,
"content_is_not_available": null,
"paper_texture": null,
"book_fold": null,
"lighting": null,
"side_pages": null,
"shadow": null,
"night_reader": null,
"single_page_view": null,
"double_page_view": null,
"reset_all_options": null,
"reading_layout": null,
"quality": null,
"channel": "Canal",
"subscribers": "Suscriptores",
"description": "Descripción",
"available_at": "Disponible en",
"search_title": "Buscar título",
"want_it": "Lo quiero!",
"have_it": "Lo tengo!",
"source": "Fuente",
"reset_all_filters": "Restablecer todos los filtros",
"only_display_items_i_have": "Sólo mostrar lo que ya tengo",
"only_display_items_i_want": "Sólo mostrar lo que quiero",
"only_display_unmarked_items": "Sólo mostrar ítems sin marcación",
"display_all_items": "Mostar todos los ítems",
"table_of_contents": "Tabla de contenido",
"no_results_message": "No hay resultados",
"all": "Todos",
"special_pages": "Páginas especiales",
"scan": "Escaneos",
"scanlation": "Escaneo/Traducción",
"scanners": "Escaneadores",
"cleaners": "Limpiadores",
"typesetters": "Maquetadores",
"notes": "Notas",
"cover": "Portada",
"tags": "Etiquetas",
"no_source_warning": "No hay fuente",
"copy_anchor_link": "Copiar enlace de anclaje",
"anchor_link_copied": "Enlace de anclaje copiado",
"folders": "Carpetas",
"empty_folder_message": "Carpeta vacía",
"switch_to_grid_view": "Cambiar a vista de retícula",
"switch_to_folder_view": "Cambiar a vista de carpeta",
"content_is_not_available": "Contenido no disponible",
"paper_texture": "Papel texturizado",
"book_fold": "Tapa dura",
"lighting": "Iluminación",
"side_pages": "Contraportada",
"shadow": "Sombra",
"night_reader": "Lector nocturno",
"single_page_view": "Vista de página única",
"double_page_view": "Vista de página doble",
"reset_all_options": "Restablecer todas las opciones",
"reading_layout": "Disposición de lectura",
"quality": "Calidad",
"only_unavailable_videos": null,
"oldest": null,
"newest": null,
@ -778,7 +716,13 @@
"most_popular": null,
"shortest": null,
"longest": null,
"search": null
"search": null,
"showing_x_out_of_y_results": null,
"return_to_x": null,
"x_results": null,
"definition_x": null,
"subitem_of_x": null,
"variant_of_x": null
}
},
{
@ -793,7 +737,6 @@
"wiki_short_description": "Uma enciclopédia com tudo relacionado a DrakeNieR",
"chronicles_short_description": "Explore as crônicas de DrakeNieR em ordem cronológica.",
"news": "Notícias",
"merch": "Mercadorias",
"gallery": "Galeria",
"archives": "Arquivos",
"about_us": "Sobre Nós",
@ -802,7 +745,6 @@
"contents_description": "",
"type": "Tipo",
"category": "Categoria",
"categories": "Categorias",
"size": "Tamanho",
"release_date": "Dia de lançamento",
"release_year": "Ano de lançamento",
@ -812,22 +754,16 @@
"height": "Altura",
"thickness": "Grossura",
"subitem": "Subitem",
"subitems": "Subitens",
"subitem_of": "Subitem de",
"variant": "Variante",
"variants": "Variantes",
"variant_of": "Variante de",
"summary": "Sumário",
"audio": "Audio",
"video": "Video",
"textual": "Textos",
"game": "Jogos",
"other": "Outros",
"return_to": "Voltar para",
"left_to_right": "Esquerda para direita",
"right_to_left": "Direita para esquerda",
"page": "Página",
"pages": "Páginas",
"page_order": "Ordem de páginas",
"binding": "Encadernação",
"type_information": "Informação do tipo",
@ -840,15 +776,12 @@
"view_scans": "Ver scans",
"paperback": "Brochura",
"hardcover": "Capa dura",
"languages": "Línguas",
"select_language": "Selecionar língua",
"language": "Língua",
"library_description": "",
"wiki_description": null,
"chronicles_description": null,
"news_description": "",
"merch_description": "",
"gallery_description": "",
"archives_description": "",
"about_us_description": "",
"page_not_found": "Página não encontrada",
@ -857,8 +790,6 @@
"show_subitems": "Mostrar subitens",
"show_primary_items": "Mostrar itens primários",
"show_secondary_items": "Mostrar itens secundários",
"no_type": "Sem tipo",
"no_year": "Sem ano",
"order_by": "Ordenar por",
"group_by": "Agrupar por",
"select_option_sidebar": "Selecione uma opção na aba lateral",
@ -889,20 +820,14 @@
"translation_notice": "Este conteúdo é uma tradução de fã.",
"source_language": "Língua original",
"pronouns": "Pronomes",
"no_category": "Sem Categoria",
"item": "Item",
"items": "Itens",
"content": "Conteúdo",
"result": "Resultado",
"results": "Resultados",
"language_switch_message": "Este conteúdo não está disponível na língua selecionada. Você pode escolher uma das seguintes línguas:",
"open_settings": "Abrir configurações",
"change_language": "Mudar língua",
"open_search": "Abrir pesquisa",
"chronology": "Cronologia",
"accords_handbook": "Livro de mão da Accord",
"legality": "Legalidade",
"members": "Membros",
"sharing_policy": "Política de compartilhamento",
"contact_us": "Fale conosco",
"email": null,
@ -935,7 +860,6 @@
"only_display_unmarked_items": null,
"display_all_items": null,
"table_of_contents": null,
"definition": null,
"no_results_message": null,
"all": null,
"special_pages": null,
@ -973,7 +897,13 @@
"most_popular": null,
"shortest": null,
"longest": null,
"search": null
"search": null,
"showing_x_out_of_y_results": null,
"return_to_x": null,
"x_results": null,
"definition_x": null,
"subitem_of_x": null,
"variant_of_x": null
}
}
]

View File

@ -1,8 +1,7 @@
import { Ico } from "./Ico";
import { ToolTip } from "./ToolTip";
import { cJoin } from "helpers/className";
import { useAtomGetter } from "helpers/atoms";
import { atoms } from "contexts/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -17,10 +16,10 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const AnchorShare = ({ id, className }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<ToolTip content={langui.copy_anchor_link} trigger="mouseenter" className="text-sm">
<ToolTip content={langui.anchor_link_copied} trigger="click" className="text-sm">
<ToolTip content={format("copy_anchor_link")} trigger="mouseenter" className="text-sm">
<ToolTip content={format("anchor_link_copied")} trigger="click" className="text-sm">
<Ico
icon="link"
className={cJoin("cursor-pointer transition-colors hover:text-dark", className)}

View File

@ -10,6 +10,7 @@ import { OpenGraph, TITLE_PREFIX, TITLE_SEPARATOR } from "helpers/openGraph";
import { Ids } from "types/ids";
import { atoms } from "contexts/atoms";
import { useAtomGetter, useAtomPair } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -48,7 +49,7 @@ export const AppLayout = ({
const [isMainPanelOpened, setMainPanelOpened] = useAtomPair(atoms.layout.mainPanelOpened);
const isMenuGesturesEnabled = useAtomGetter(atoms.layout.menuGesturesEnabled);
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const isScreenAtLeastXs = useAtomGetter(atoms.containerQueries.isScreenAtLeastXs);
@ -149,7 +150,7 @@ export const AppLayout = ({
{isDefined(contentPanel) ? (
contentPanel
) : (
<ContentPlaceholder message={langui.select_option_sidebar ?? ""} icon={"chevron_left"} />
<ContentPlaceholder message={format("select_option_sidebar")} icon={"chevron_left"} />
)}
</div>

View File

@ -3,6 +3,7 @@ import { PageSelector } from "components/Inputs/PageSelector";
import { atoms } from "contexts/atoms";
import { isUndefined } from "helpers/asserts";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { Ids } from "types/ids";
@ -48,14 +49,14 @@ export const Paginator = ({
const DefaultRenderWhenEmpty = () => {
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<div className="grid h-full place-content-center">
<div
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
border-dark p-8 text-dark opacity-40">
{is3ColumnsLayout && <Ico icon="chevron_left" className="!text-[300%]" />}
<p className="max-w-xs text-2xl">{langui.no_results_message}</p>
<p className="max-w-xs text-2xl">{format("no_results_message")}</p>
{!is3ColumnsLayout && <Ico icon="chevron_right" className="!text-[300%]" />}
</div>
</div>

View File

@ -3,8 +3,7 @@ import { ToolTip } from "components/ToolTip";
import { LibraryItemUserStatus } from "types/types";
import { cIf, cJoin } from "helpers/className";
import { useLibraryItemUserStatus } from "hooks/useLibraryItemUserStatus";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -20,7 +19,7 @@ interface Props {
export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
const { libraryItemUserStatus, setLibraryItemUserStatus } = useLibraryItemUserStatus();
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<div
@ -28,10 +27,10 @@ export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
"flex flex-row flex-wrap place-content-center place-items-center",
cIf(expand, "gap-4", "gap-2")
)}>
<ToolTip content={langui.want_it} disabled={expand}>
<ToolTip content={format("want_it")} disabled={expand}>
<Button
icon="favorite"
text={expand ? langui.want_it : undefined}
text={expand ? format("want_it") : undefined}
active={libraryItemUserStatus[id] === LibraryItemUserStatus.Want}
onClick={(event) => {
event.preventDefault();
@ -46,10 +45,10 @@ export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
}}
/>
</ToolTip>
<ToolTip content={langui.have_it} disabled={expand}>
<ToolTip content={format("have_it")} disabled={expand}>
<Button
icon="back_hand"
text={expand ? langui.have_it : undefined}
text={expand ? format("have_it") : undefined}
active={libraryItemUserStatus[id] === LibraryItemUserStatus.Have}
onClick={(event) => {
event.preventDefault();

View File

@ -15,6 +15,7 @@ import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { Link } from "components/Inputs/Link";
import { useFormat } from "hooks/useFormat";
/*
*
@ -225,7 +226,7 @@ export const TableOfContents = ({
title,
horizontalLine = false,
}: TableOfContentsProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
return (
@ -233,7 +234,7 @@ export const TableOfContents = ({
{toc.children.length > 0 && (
<>
{horizontalLine && <HorizontalLine />}
<h3 className="text-xl">{langui.table_of_contents}</h3>
<h3 className="text-xl">{format("table_of_contents")}</h3>
<div className="max-w-[14.5rem] text-left">
<p
className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap

View File

@ -5,6 +5,7 @@ import { useSmartLanguage } from "hooks/useSmartLanguage";
import { isUndefined } from "helpers/asserts";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -22,7 +23,7 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const ReturnButton = ({ href, title, displayOnlyOn, className }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
return (
@ -31,7 +32,7 @@ export const ReturnButton = ({ href, title, displayOnlyOn, className }: Props):
(!is3ColumnsLayout && displayOnlyOn === "1ColumnLayout") ||
isUndefined(displayOnlyOn)) && (
<div className={className}>
<Button href={href} text={`${langui.return_to} ${title}`} icon="navigate_before" />
<Button href={href} text={format("return_to_x", { x: title })} icon="navigate_before" />
</div>
)}
</>

View File

@ -10,6 +10,7 @@ import { ColoredSvg } from "components/ColoredSvg";
import { atoms } from "contexts/atoms";
import { useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms";
import { Markdawn } from "components/Markdown/Markdawn";
import { useFormat } from "hooks/useFormat";
/*
*
@ -18,7 +19,7 @@ import { Markdawn } from "components/Markdown/Markdawn";
export const MainPanel = (): JSX.Element => {
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const [isMainPanelReduced, setMainPanelReduced] = useAtomPair(atoms.layout.mainPanelReduced);
const setSettingsOpened = useAtomSetter(atoms.layout.settingsOpened);
const setSearchOpened = useAtomSetter(atoms.layout.searchOpened);
@ -72,7 +73,7 @@ export const MainPanel = (): JSX.Element => {
cIf(isMainPanelReduced && is3ColumnsLayout, "flex-col gap-3", "flex-row")
)}>
<ToolTip
content={<h3 className="text-2xl">{langui.open_settings}</h3>}
content={<h3 className="text-2xl">{format("open_settings")}</h3>}
placement={isMainPanelReduced ? "right" : "top"}>
<Button
onClick={() => {
@ -83,7 +84,7 @@ export const MainPanel = (): JSX.Element => {
/>
</ToolTip>
<ToolTip
content={<h3 className="text-2xl">{langui.open_search}</h3>}
content={<h3 className="text-2xl">{format("open_search")}</h3>}
placement={isMainPanelReduced ? "right" : "top"}>
<Button
onClick={() => {
@ -102,32 +103,32 @@ export const MainPanel = (): JSX.Element => {
<NavOption
url="/library"
icon="auto_stories"
title={langui.library}
subtitle={langui.library_short_description}
title={format("library")}
subtitle={format("library_short_description")}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<NavOption
url="/contents"
icon="workspaces"
title={langui.contents}
subtitle={langui.contents_short_description}
title={format("contents")}
subtitle={format("contents_short_description")}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<NavOption
url="/wiki"
icon="travel_explore"
title={langui.wiki}
subtitle={langui.wiki_short_description}
title={format("wiki")}
subtitle={format("wiki_short_description")}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<NavOption
url="/chronicles"
icon="schedule"
title={langui.chronicles}
subtitle={langui.chronicles_short_description}
title={format("chronicles")}
subtitle={format("chronicles_short_description")}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
@ -136,7 +137,7 @@ export const MainPanel = (): JSX.Element => {
<NavOption
url="/news"
icon="newspaper"
title={langui.news}
title={format("news")}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
@ -144,7 +145,7 @@ export const MainPanel = (): JSX.Element => {
<NavOption
url="/merch"
icon="store"
title={langui.merch}
title={format("merch")}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
*/}
@ -152,30 +153,30 @@ export const MainPanel = (): JSX.Element => {
<NavOption
url="https://gallery.accords-library.com/posts/"
icon="perm_media"
title={langui.gallery}
title={format("gallery")}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<NavOption
url="/archives"
icon="save"
title={langui.archives}
title={format("archives")}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<NavOption
url="/about-us"
icon="info"
title={langui.about_us}
title={format("about_us")}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
{(!isMainPanelReduced || !is3ColumnsLayout) && <HorizontalLine />}
<div className={cJoin("text-center", cIf(isMainPanelReduced && is3ColumnsLayout, "hidden"))}>
{isDefinedAndNotEmpty(langui.licensing_notice) && (
{isDefinedAndNotEmpty(format("licensing_notice")) && (
<p>
<Markdawn text={langui.licensing_notice} />
<Markdawn text={format("licensing_notice")} />
</p>
)}
<div className="mt-4 mb-8 grid place-content-center">
@ -199,9 +200,9 @@ export const MainPanel = (): JSX.Element => {
/>
</Link>
</div>
{isDefinedAndNotEmpty(langui.copyright_notice) && (
{isDefinedAndNotEmpty(format("copyright_notice")) && (
<p>
<Markdawn text={langui.copyright_notice} />
<Markdawn text={format("copyright_notice")} />
</p>
)}
<div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8">

View File

@ -3,7 +3,7 @@ import { MaterialSymbol } from "material-symbols";
import { Popup } from "components/Containers/Popup";
import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms";
import { useAtomPair, useAtomGetter } from "helpers/atoms";
import { useAtomPair } from "helpers/atoms";
import { TextInput } from "components/Inputs/TextInput";
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search";
import { PreviewCard, TranslatedPreviewCard } from "components/PreviewCard";
@ -20,6 +20,7 @@ import { getVideoThumbnailURL } from "helpers/videos";
import { UpPressable } from "components/Containers/UpPressable";
import { prettyItemSubType, prettySlug } from "helpers/formatters";
import { Ico } from "components/Ico";
import { useFormat } from "hooks/useFormat";
/*
*
@ -36,7 +37,7 @@ const SEARCH_LIMIT = 8;
export const SearchPopup = (): JSX.Element => {
const [isSearchOpened, setSearchOpened] = useAtomPair(atoms.layout.searchOpened);
const [query, setQuery] = useState("");
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const [libraryItems, setLibraryItems] = useState<CustomSearchResponse<MeiliLibraryItem>>();
const [contents, setContents] = useState<CustomSearchResponse<MeiliContent>>();
const [videos, setVideos] = useState<CustomSearchResponse<MeiliVideo>>();
@ -178,14 +179,14 @@ export const SearchPopup = (): JSX.Element => {
fillViewport>
<h2 className="inline-flex place-items-center gap-2 text-2xl">
<Ico icon="search" isFilled />
{langui.search}
{format("search")}
</h2>
<TextInput onChange={setQuery} value={query} placeholder={langui.search_title} />
<TextInput onChange={setQuery} value={query} placeholder={format("search_title")} />
<div className="flex w-full flex-wrap gap-12 gap-x-16">
{isDefined(libraryItems) && (
<SearchResultSection
title={langui.library}
title={format("library")}
icon="auto_stories"
href={`/library?page=1&query=${query}\
&sort=0&primary=true&secondary=true&subitems=true&status=all`}
@ -232,7 +233,7 @@ export const SearchPopup = (): JSX.Element => {
{isDefined(contents) && (
<SearchResultSection
title={langui.contents}
title={format("contents")}
icon="workspaces"
href={`/contents/all?page=1&query=${query}&sort=0`}
totalHits={contents.estimatedTotalHits}>
@ -276,7 +277,7 @@ export const SearchPopup = (): JSX.Element => {
{isDefined(wikiPages) && (
<SearchResultSection
title={langui.wiki}
title={format("wiki")}
icon="travel_explore"
href={`/wiki?page=1&query=${query}`}
totalHits={wikiPages.estimatedTotalHits}>
@ -327,7 +328,7 @@ export const SearchPopup = (): JSX.Element => {
{isDefined(posts) && (
<SearchResultSection
title={langui.news}
title={format("news")}
icon="newspaper"
href={`/news?page=1&query=${query}`}
totalHits={posts.estimatedTotalHits}>
@ -369,7 +370,7 @@ export const SearchPopup = (): JSX.Element => {
{isDefined(videos) && (
<SearchResultSection
title={langui.videos}
title={format("videos")}
icon="movie"
href={`/archives/videos?page=1&query=${query}&sort=1&gone=`}
totalHits={videos.estimatedTotalHits}>
@ -424,26 +425,30 @@ const SearchResultSection = ({
href,
totalHits,
children,
}: SearchResultSectionProps) => (
<>
{isDefined(totalHits) && totalHits > 0 && (
<div>
<div className="mb-6 grid place-content-start">
<UpPressable
className="grid grid-cols-[auto_1fr] place-items-center gap-6 px-6 py-4"
href={href}>
<Ico icon={icon} className="!text-3xl" isFilled />
<div>
<p className="font-headers text-lg">{title}</p>
{isDefined(totalHits) && totalHits > SEARCH_LIMIT && (
/* TODO: Langui */
<p className="text-sm">{`(showing ${SEARCH_LIMIT} out of ${totalHits} results)`}</p>
)}
</div>
</UpPressable>
}: SearchResultSectionProps) => {
const { format } = useFormat();
return (
<>
{isDefined(totalHits) && totalHits > 0 && (
<div>
<div className="mb-6 grid place-content-start">
<UpPressable
className="grid grid-cols-[auto_1fr] place-items-center gap-6 px-6 py-4"
href={href}>
<Ico icon={icon} className="!text-3xl" isFilled />
<div>
<p className="font-headers text-lg">{title}</p>
{isDefined(totalHits) && totalHits > SEARCH_LIMIT && (
<p className="text-sm">
({format("showing_x_out_of_y_results", { x: SEARCH_LIMIT, y: totalHits })})
</p>
)}
</div>
</UpPressable>
</div>
{children}
</div>
{children}
</div>
)}
</>
);
)}
</>
);
};

View File

@ -14,6 +14,7 @@ import { atoms } from "contexts/atoms";
import { useAtomGetter, useAtomPair } from "helpers/atoms";
import { ThemeMode } from "contexts/settings";
import { Ico } from "components/Ico";
import { useFormat } from "hooks/useFormat";
export const SettingsPopup = (): JSX.Element => {
const [preferredLanguages, setPreferredLanguages] = useAtomPair(
@ -27,7 +28,7 @@ export const SettingsPopup = (): JSX.Element => {
const [themeMode, setThemeMode] = useAtomPair(atoms.settings.themeMode);
const languages = useAtomGetter(atoms.localData.languages);
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const currencies = useAtomGetter(atoms.localData.currencies);
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
@ -52,7 +53,7 @@ export const SettingsPopup = (): JSX.Element => {
}}>
<h2 className="inline-flex place-items-center gap-2 text-2xl">
<Ico icon="discover_tune" isFilled />
{langui.settings}
{format("settings")}
</h2>
<div
@ -62,7 +63,7 @@ export const SettingsPopup = (): JSX.Element => {
)}>
{router.locales && (
<div>
<h3 className="text-xl">{langui.languages}</h3>
<h3 className="text-xl">{format("language", { count: preferredLanguages.length })}</h3>
{preferredLanguages.length > 0 && (
<OrderableList
items={preferredLanguages.map((locale) => ({
@ -72,11 +73,11 @@ export const SettingsPopup = (): JSX.Element => {
insertLabels={[
{
insertAt: 0,
name: langui.primary_language ?? "Primary language",
name: format("primary_language"),
},
{
insertAt: 1,
name: langui.secondary_language ?? "Secondary languages",
name: format("secondary_language"),
},
]}
onChange={(items) => {
@ -94,7 +95,7 @@ export const SettingsPopup = (): JSX.Element => {
cIf(!is1ColumnLayout, "grid-cols-2")
)}>
<div>
<h3 className="text-xl">{langui.theme}</h3>
<h3 className="text-xl">{format("theme")}</h3>
<ButtonGroup
buttonsProps={[
{
@ -103,7 +104,7 @@ export const SettingsPopup = (): JSX.Element => {
sendAnalytics("Settings", "Change theme (light)");
},
active: themeMode === ThemeMode.Light,
text: langui.light,
text: format("light"),
},
{
onClick: () => {
@ -111,7 +112,7 @@ export const SettingsPopup = (): JSX.Element => {
sendAnalytics("Settings", "Change theme (auto)");
},
active: themeMode === ThemeMode.Auto,
text: langui.auto,
text: format("auto"),
},
{
onClick: () => {
@ -119,14 +120,14 @@ export const SettingsPopup = (): JSX.Element => {
sendAnalytics("Settings", "Change theme (dark)");
},
active: themeMode === ThemeMode.Dark,
text: langui.dark,
text: format("dark"),
},
]}
/>
</div>
<div>
<h3 className="text-xl">{langui.currency}</h3>
<h3 className="text-xl">{format("currency")}</h3>
<div>
<Select
options={currencyOptions}
@ -144,7 +145,7 @@ export const SettingsPopup = (): JSX.Element => {
</div>
<div>
<h3 className="text-xl">{langui.font_size}</h3>
<h3 className="text-xl">{format("font_size")}</h3>
<ButtonGroup
buttonsProps={[
{
@ -185,7 +186,7 @@ export const SettingsPopup = (): JSX.Element => {
</div>
<div>
<h3 className="text-xl">{langui.font}</h3>
<h3 className="text-xl">{format("font")}</h3>
<div className="grid gap-2">
<Button
active={!isDyslexic}
@ -209,7 +210,7 @@ export const SettingsPopup = (): JSX.Element => {
</div>
<div>
<h3 className="text-xl">{langui.player_name}</h3>
<h3 className="text-xl">{format("player_name")}</h3>
<TextInput
placeholder="<player>"
className="w-48"

View File

@ -11,11 +11,9 @@ import { ThumbnailHeader } from "./ThumbnailHeader";
import { ToolTip } from "./ToolTip";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { PostWithTranslations } from "types/types";
import { getStatusDescription } from "helpers/others";
import { filterHasAttributes } from "helpers/asserts";
import { prettySlug } from "helpers/formatters";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -50,7 +48,8 @@ export const PostPage = ({
displayTitle = true,
...otherProps
}: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format, formatStatusDescription } = useFormat();
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: post.translations,
languageExtractor: useCallback(
@ -79,10 +78,10 @@ export const PostPage = ({
{selectedTranslation && (
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
<p className="font-headers font-bold">{langui.status}:</p>
<p className="font-headers font-bold">{format("status")}:</p>
<ToolTip
content={getStatusDescription(selectedTranslation.status, langui)}
content={formatStatusDescription(selectedTranslation.status)}
maxWidth={"20rem"}>
<Chip text={selectedTranslation.status} />
</ToolTip>

View File

@ -6,8 +6,7 @@ import { Chip } from "components/Chip";
import { RecorderChipFragment } from "graphql/generated";
import { ImageQuality } from "helpers/img";
import { filterHasAttributes } from "helpers/asserts";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -22,7 +21,7 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const RecorderChip = ({ recorder }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<ToolTip
@ -40,7 +39,7 @@ export const RecorderChip = ({ recorder }: Props): JSX.Element => {
<h3 className=" text-2xl">{recorder.username}</h3>
{recorder.languages?.data && recorder.languages.data.length > 0 && (
<div className="flex flex-row flex-wrap gap-1">
<p>{langui.languages}:</p>
<p>{format("language", { count: recorder.languages.data.length })}:</p>
{filterHasAttributes(recorder.languages.data, ["attributes"] as const).map(
(language) => (
<Fragment key={language.__typename}>
@ -52,7 +51,7 @@ export const RecorderChip = ({ recorder }: Props): JSX.Element => {
)}
{recorder.pronouns && (
<div className="flex flex-row flex-wrap gap-1">
<p>{langui.pronouns}:</p>
<p>{format("pronouns")}:</p>
<Chip text={recorder.pronouns} />
</div>
)}

View File

@ -10,6 +10,7 @@ import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { Ids } from "types/ids";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
interface Group<T> {
name: string;
@ -71,7 +72,7 @@ export const SmartList = <T,>({
className,
}: Props<T>): JSX.Element => {
const [page, setPage] = useState(1);
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
useScrollTopOnChange(Ids.ContentPanel, [page], paginationScroolTop);
useEffect(() => setPage(1), [searchingTerm, groupingFunction, groupSortingFunction]);
@ -180,13 +181,7 @@ export const SmartList = <T,>({
className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl
first-of-type:pt-0">
{group.name}
<Chip
text={`${group.totalCount} ${
group.items.length <= 1
? langui.result?.toLowerCase() ?? ""
: langui.results?.toLowerCase() ?? ""
}`}
/>
<Chip text={format("x_results", { x: group.totalCount })} />
</h2>
)}
<div
@ -222,15 +217,15 @@ export const SmartList = <T,>({
*/
const DefaultRenderWhenEmpty = () => {
const { format } = useFormat();
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
const langui = useAtomGetter(atoms.localData.langui);
return (
<div className="grid h-full place-content-center">
<div
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
border-dark p-8 text-dark opacity-40">
{is3ColumnsLayout && <Ico icon="chevron_left" className="!text-[300%]" />}
<p className="max-w-xs text-2xl">{langui.no_results_message}</p>
<p className="max-w-xs text-2xl">{format("no_results_message")}</p>
{!is3ColumnsLayout && <Ico icon="chevron_right" className="!text-[300%]" />}
</div>
</div>

View File

@ -8,6 +8,7 @@ import { ImageQuality } from "helpers/img";
import { filterHasAttributes } from "helpers/asserts";
import { useAtomGetter } from "helpers/atoms";
import { atoms } from "contexts/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -42,7 +43,7 @@ export const ThumbnailHeader = ({
description,
languageSwitcher,
}: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const { showLightBox } = useAtomGetter(atoms.lightBox);
return (
@ -72,7 +73,7 @@ export const ThumbnailHeader = ({
<div className="flew-wrap flex flex-row place-content-center gap-8">
{type?.data?.attributes && (
<div className="flex flex-col place-items-center gap-2">
<h3 className="text-xl">{langui.type}</h3>
<h3 className="text-xl">{format("type", { count: 1 })}</h3>
<div className="flex flex-row flex-wrap">
<Chip
text={
@ -85,7 +86,7 @@ export const ThumbnailHeader = ({
{categories && categories.data.length > 0 && (
<div className="flex flex-col place-items-center gap-2">
<h3 className="text-xl">{langui.categories}</h3>
<h3 className="text-xl">{format("category", { count: categories.data.length })}</h3>
<div className="flex flex-row flex-wrap place-content-center gap-2">
{filterHasAttributes(categories.data, ["attributes", "id"] as const).map(
(category) => (

View File

@ -1,12 +1,12 @@
import { useCallback } from "react";
import { Chip } from "components/Chip";
import { ToolTip } from "components/ToolTip";
import { getStatusDescription } from "helpers/others";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { Button } from "components/Inputs/Button";
import { cIf, cJoin } from "helpers/className";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { ContentStatus, useFormat } from "hooks/useFormat";
/*
*
@ -21,7 +21,7 @@ interface Props {
translations: {
language: string | undefined;
definition: string | null | undefined;
status: string | undefined;
status: ContentStatus | undefined;
}[];
index: number;
categories: string[];
@ -31,7 +31,7 @@ interface Props {
const DefinitionCard = ({ source, translations = [], index, categories }: Props): JSX.Element => {
const isContentPanelAtLeastMd = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastMd);
const langui = useAtomGetter(atoms.localData.langui);
const { format, formatStatusDescription } = useFormat();
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: translations,
languageExtractor: useCallback((item: Props["translations"][number]) => item.language, []),
@ -40,7 +40,7 @@ const DefinitionCard = ({ source, translations = [], index, categories }: Props)
return (
<>
<div className="flex flex-wrap place-items-center gap-2">
<p className="font-headers text-lg font-bold">{`${langui.definition} ${index}`}</p>
<p className="font-headers text-lg font-bold">{format("definition_x", { x: index })}</p>
{translations.length > 1 && (
<>
@ -53,7 +53,7 @@ const DefinitionCard = ({ source, translations = [], index, categories }: Props)
<>
<Separator />
<ToolTip
content={getStatusDescription(selectedTranslation.status, langui)}
content={formatStatusDescription(selectedTranslation.status)}
maxWidth={"20rem"}>
<Chip text={selectedTranslation.status} />
</ToolTip>
@ -80,7 +80,7 @@ const DefinitionCard = ({ source, translations = [], index, categories }: Props)
"mt-3 flex place-items-center gap-2",
cIf(!isContentPanelAtLeastMd, "flex-col text-center")
)}>
<p>{langui.source}: </p>
<p>{format("source")}: </p>
<Button href={source.url} size="small" text={source.name} />
</div>
)}

View File

@ -21,11 +21,13 @@ import { LocalDataFile } from "graphql/fetchLocalData";
const languages = atomPairing(atom<Languages>([]));
const currencies = atomPairing(atom<Currencies>([]));
const langui = atomPairing(atom<Langui>({}));
const fallbackLangui = atomPairing(atom<Langui>({}));
export const localData = {
languages: languages[0],
currencies: currencies[0],
langui: langui[0],
fallbackLangui: fallbackLangui[0],
};
const getFileName = (name: LocalDataFile): string => `/local-data/${name}.json`;
@ -34,6 +36,7 @@ export const useLocalData = (): void => {
const setLanguages = useAtomSetter(languages);
const setCurrencies = useAtomSetter(currencies);
const setLangui = useAtomSetter(langui);
const setFallbackLangui = useAtomSetter(fallbackLangui);
const { locale } = useRouter();
const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
@ -56,4 +59,9 @@ export const useLocalData = (): void => {
console.log("[useLocalData] Refresh langui");
setLangui(processLangui(rawLangui, locale));
}, [locale, rawLangui, setLangui]);
useEffect(() => {
console.log("[useLocalData] Refresh fallback langui");
setFallbackLangui(processLangui(rawLangui, "en"));
}, [rawLangui, setFallbackLangui]);
};

View File

@ -1,6 +1,5 @@
import { GetStaticProps } from "next";
import { getReadySdk } from "./sdk";
import { getLangui } from "./fetchLocalData";
import { PostWithTranslations } from "types/types";
import { getOpenGraph } from "helpers/openGraph";
import { prettyDate, prettySlug } from "helpers/formatters";
@ -8,6 +7,7 @@ import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/local
import { filterHasAttributes, isDefined } from "helpers/asserts";
import { getDescription } from "helpers/description";
import { AppLayoutRequired } from "components/AppLayout";
import { getFormat } from "helpers/i18n";
export interface PostStaticProps extends AppLayoutRequired {
post: PostWithTranslations;
@ -17,7 +17,7 @@ export const getPostStaticProps =
(slug: string): GetStaticProps =>
async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const post = await sdk.getPost({
slug: slug,
language_code: context.locale ?? "en",
@ -38,10 +38,8 @@ export const getPostStaticProps =
const title = selectedTranslation?.title ?? prettySlug(slug);
const description = getDescription(selectedTranslation?.excerpt, {
[langui.release_date ?? "Release date"]: [
prettyDate(post.posts.data[0].attributes.date, context.locale),
],
[langui.categories ?? "Categories"]: filterHasAttributes(
[format("release_date")]: [prettyDate(post.posts.data[0].attributes.date, context.locale)],
[format("category", { count: Infinity })]: filterHasAttributes(
post.posts.data[0].attributes.categories?.data,
["attributes"] as const
).map((category) => category.attributes.short),
@ -53,7 +51,7 @@ export const getPostStaticProps =
const props: PostStaticProps = {
post: post.posts.data[0].attributes as PostWithTranslations,
openGraph: getOpenGraph(langui, title, description, thumbnail),
openGraph: getOpenGraph(format, title, description, thumbnail),
};
return {
props: props,

181
src/graphql/icuParams.ts Normal file
View File

@ -0,0 +1,181 @@
export interface ICUParams {
library: never;
contents: never;
wiki: never;
chronicles: never;
library_short_description: never;
contents_short_description: never;
wiki_short_description: never;
chronicles_short_description: never;
news: never;
gallery: never;
archives: never;
about_us: never;
licensing_notice: never;
copyright_notice: never;
contents_description: never;
type: { count: number };
category: { count: number };
size: never;
release_date: never;
release_year: never;
details: never;
price: never;
width: never;
height: never;
thickness: never;
subitem: { count: number };
variant: { count: number };
summary: never;
audio: never;
video: never;
textual: never;
game: never;
other: never;
left_to_right: never;
right_to_left: never;
page: { count: number };
page_order: never;
binding: never;
type_information: never;
front_matter: never;
back_matter: never;
open_content: never;
read_content: never;
watch_content: never;
listen_content: never;
view_scans: never;
paperback: never;
hardcover: never;
select_language: never;
language: { count: number };
library_description: never;
wiki_description: never;
chronicles_description: never;
news_description: never;
archives_description: never;
about_us_description: never;
page_not_found: never;
default_description: never;
name: never;
show_subitems: never;
show_primary_items: never;
show_secondary_items: never;
order_by: never;
group_by: never;
select_option_sidebar: never;
group: never;
settings: never;
theme: never;
light: never;
auto: never;
dark: never;
font_size: never;
player_name: never;
currency: never;
font: never;
calculated: never;
status_incomplete: never;
status_draft: never;
status_review: never;
status_done: never;
incomplete: never;
draft: never;
review: never;
done: never;
status: never;
transcribers: never;
translators: never;
proofreaders: never;
transcript_notice: never;
translation_notice: never;
source_language: never;
pronouns: never;
item: { count: number };
content: never;
open_settings: never;
change_language: never;
open_search: never;
chronology: never;
accords_handbook: never;
legality: never;
sharing_policy: never;
contact_us: never;
email: never;
email_gdpr_notice: never;
message: never;
send: never;
response_invalid_code: never;
response_invalid_email: never;
response_email_success: never;
always_show_info: never;
item_not_available: never;
primary_language: never;
secondary_language: never;
combine_related_contents: never;
previous_content: never;
followup_content: never;
videos: never;
view_on: never;
channel: never;
subscribers: never;
description: never;
available_at: never;
search_title: never;
want_it: never;
have_it: never;
source: never;
reset_all_filters: never;
only_display_items_i_have: never;
only_display_items_i_want: never;
only_display_unmarked_items: never;
display_all_items: never;
table_of_contents: never;
no_results_message: never;
all: never;
special_pages: never;
scan: never;
scanlation: never;
scanners: never;
cleaners: never;
typesetters: never;
notes: never;
cover: never;
tags: never;
no_source_warning: never;
copy_anchor_link: never;
anchor_link_copied: never;
folders: never;
empty_folder_message: never;
switch_to_grid_view: never;
switch_to_folder_view: never;
content_is_not_available: never;
paper_texture: never;
book_fold: never;
lighting: never;
side_pages: never;
shadow: never;
night_reader: never;
single_page_view: never;
double_page_view: never;
reset_all_options: never;
reading_layout: never;
quality: never;
only_unavailable_videos: never;
oldest: never;
newest: never;
least_popular: never;
most_popular: never;
shortest: never;
longest: never;
search: never;
showing_x_out_of_y_results: {
x: Date | boolean | number | string;
y: Date | boolean | number | string;
};
return_to_x: { x: undefined | null | Date | boolean | number | string };
x_results: { x: number };
definition_x: { x: Date | boolean | number | string };
subitem_of_x: { x: Date | boolean | number | string };
variant_of_x: { x: Date | boolean | number | string };
}

View File

@ -0,0 +1,53 @@
/* eslint-disable import/no-nodejs-modules */
import { createWriteStream } from "fs";
import { parse, TYPE } from "@formatjs/icu-messageformat-parser";
import { getLangui } from "./fetchLocalData";
import { filterDefined } from "helpers/asserts";
const OUTPUT_FOLDER = `${process.cwd()}/src/graphql`;
const icuToTypescript = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ui_language, ...langui } = getLangui("en");
const output = createWriteStream(`${OUTPUT_FOLDER}/icuParams.ts`);
output.write("export interface ICUParams {\n");
Object.keys(langui).map((oKey) => {
const key = oKey as keyof typeof langui;
const parsedMessage = parse(langui[key] ?? "");
const variables = filterDefined(
parsedMessage.map((elem) => {
if (elem.type === TYPE.argument) {
return `${elem.value}: Date | boolean | number | string`;
} else if (elem.type === TYPE.plural) {
return `${elem.value}: number`;
} else if (elem.type === TYPE.select) {
const options = Object.keys(elem.options);
const stringOptions = options.filter(
(option) => option !== "undefined" && option !== "other"
);
const type: string[] = stringOptions.map((option) => `"${option}"`);
if (options.includes("undefined")) type.push(...["undefined", "null"]);
if (options.includes("other")) type.push(...["Date", "boolean", "number", "string"]);
return `${elem.value}: ${type.join(` | `)}`;
}
return undefined;
})
);
const variablesType = variables.length > 0 ? `{ ${variables.join(";")} }` : "never";
output.write(` ${key}: ${variablesType};\n`);
});
output.write("}\n");
console.log(`${OUTPUT_FOLDER}/icu-params.ts has been written!`);
};
if (process.argv[2] === "--icu") {
icuToTypescript();
}

View File

@ -18,7 +18,6 @@ query localDataGetWebsiteInterfaces {
wiki_short_description
chronicles_short_description
news
merch
gallery
archives
about_us
@ -27,7 +26,6 @@ query localDataGetWebsiteInterfaces {
contents_description
type
category
categories
size
release_date
release_year
@ -37,22 +35,16 @@ query localDataGetWebsiteInterfaces {
height
thickness
subitem
subitems
subitem_of
variant
variants
variant_of
summary
audio
video
textual
game
other
return_to
left_to_right
right_to_left
page
pages
page_order
binding
type_information
@ -65,15 +57,12 @@ query localDataGetWebsiteInterfaces {
view_scans
paperback
hardcover
languages
select_language
language
library_description
wiki_description
chronicles_description
news_description
merch_description
gallery_description
archives_description
about_us_description
page_not_found
@ -82,8 +71,6 @@ query localDataGetWebsiteInterfaces {
show_subitems
show_primary_items
show_secondary_items
no_type
no_year
order_by
group_by
select_option_sidebar
@ -114,20 +101,14 @@ query localDataGetWebsiteInterfaces {
translation_notice
source_language
pronouns
no_category
item
items
content
result
results
language_switch_message
open_settings
change_language
open_search
chronology
accords_handbook
legality
members
sharing_policy
contact_us
email
@ -160,7 +141,6 @@ query localDataGetWebsiteInterfaces {
only_display_unmarked_items
display_all_items
table_of_contents
definition
no_results_message
all
special_pages
@ -199,6 +179,12 @@ query localDataGetWebsiteInterfaces {
shortest
longest
search
showing_x_out_of_y_results
return_to_x
x_results
definition_x
subitem_of_x
variant_of_x
}
}
}

View File

@ -1,7 +1,7 @@
import { convertPrice } from "./numbers";
import { isDefinedAndNotEmpty, isUndefined } from "./asserts";
import { datePickerToDate } from "./date";
import { Currencies, Languages, Langui } from "./localData";
import { Currencies, Languages } from "./localData";
import { DatePickerFragment, PricePickerFragment } from "graphql/generated";
export const prettyDate = (
@ -58,25 +58,6 @@ export const prettyInlineTitle = (
return result;
};
export const prettyItemType = (metadata: { __typename: string }, langui: Langui): string => {
switch (metadata.__typename) {
case "ComponentMetadataAudio":
return langui.audio ?? "Audio";
case "ComponentMetadataBooks":
return langui.textual ?? "Textual";
case "ComponentMetadataGame":
return langui.game ?? "Game";
case "ComponentMetadataVideo":
return langui.video ?? "Video";
case "ComponentMetadataGroup":
return langui.group ?? "Group";
case "ComponentMetadataOther":
return langui.other ?? "Other";
default:
return "";
}
};
/* eslint-disable id-denylist */
export const prettyItemSubType = (
metadata:

79
src/helpers/i18n.ts Normal file
View File

@ -0,0 +1,79 @@
import { IntlMessageFormat } from "intl-messageformat";
import { LibraryItemMetadataDynamicZone } from "graphql/generated";
import { ICUParams } from "graphql/icuParams";
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { getLangui } from "graphql/fetchLocalData";
type WordingKey = keyof ICUParams;
type LibraryItemType = Exclude<LibraryItemMetadataDynamicZone["__typename"], undefined>;
type ContentStatus = "Done" | "Draft" | "Incomplete" | "Review";
const componentMetadataToWording: Record<LibraryItemType, WordingKey> = {
ComponentMetadataAudio: "audio",
ComponentMetadataBooks: "textual",
ComponentMetadataGame: "game",
ComponentMetadataGroup: "group",
ComponentMetadataVideo: "video",
ComponentMetadataOther: "other",
Error: "item",
};
const componentSetsTextsetStatusToWording: Record<
ContentStatus,
{ label: WordingKey; description: WordingKey }
> = {
Draft: { label: "draft", description: "status_draft" },
Incomplete: { label: "incomplete", description: "status_incomplete" },
Review: { label: "review", description: "status_review" },
Done: { label: "done", description: "status_done" },
};
export const getFormat = (
locale: string | undefined
): {
format: <K extends WordingKey>(
key: K,
...values: ICUParams[K] extends never ? [undefined?] : [ICUParams[K]]
) => string;
formatLibraryItemType: (metadata: { __typename: LibraryItemType }) => string;
formatStatusLabel: (status: ContentStatus) => string;
formatStatusDescription: (status: ContentStatus) => string;
} => {
const langui = getLangui(locale);
const fallbackLangui = getLangui("en");
const format = (
key: WordingKey,
values?: Record<string, Date | boolean | number | string | null | undefined>
): string => {
const processedValues = Object.fromEntries(
Object.entries(values ?? {}).map(([oKey, value]) => [
oKey,
isDefined(value) ? value : "undefined",
])
);
const result = new IntlMessageFormat(langui[key] ?? "").format(processedValues).toString();
if (isDefinedAndNotEmpty(result)) {
return result;
}
return new IntlMessageFormat(fallbackLangui[key] ?? "").format(processedValues).toString();
};
const formatLibraryItemType = (metadata: { __typename: LibraryItemType }): string =>
format(componentMetadataToWording[metadata.__typename]);
const formatStatusLabel = (status: ContentStatus): string =>
format(componentSetsTextsetStatusToWording[status].label);
const formatStatusDescription = (status: ContentStatus): string =>
format(componentSetsTextsetStatusToWording[status].description);
return {
format,
formatLibraryItemType,
formatStatusLabel,
formatStatusDescription,
};
};

View File

@ -1,7 +1,8 @@
import { OgImage, getImgSizesByQuality, ImageQuality, getAssetURL } from "./img";
import { isDefinedAndNotEmpty } from "./asserts";
import { Langui } from "./localData";
import { getFormat } from "./i18n";
import { UploadImageFragment } from "graphql/generated";
import { useFormat } from "hooks/useFormat";
const DEFAULT_OG_THUMBNAIL = {
image: `${process.env.NEXT_PUBLIC_URL_SELF}/default_og.jpg`,
@ -20,13 +21,13 @@ export interface OpenGraph {
}
export const getOpenGraph = (
langui: Langui,
format: ReturnType<typeof getFormat>["format"] | ReturnType<typeof useFormat>["format"],
title?: string | null | undefined,
description?: string | null | undefined,
thumbnail?: UploadImageFragment | null | undefined
): OpenGraph => ({
title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) ? `${TITLE_SEPARATOR}${title}` : ""}`,
description: isDefinedAndNotEmpty(description) ? description : langui.default_description ?? "",
description: isDefinedAndNotEmpty(description) ? description : format("default_description"),
thumbnail: thumbnail ? getOgImage(thumbnail) : DEFAULT_OG_THUMBNAIL,
});

View File

@ -1,10 +1,5 @@
import { Langui } from "./localData";
import { isDefined } from "./asserts";
import {
Enum_Componentsetstextset_Status,
GetLibraryItemQuery,
GetLibraryItemScansQuery,
} from "graphql/generated";
import { GetLibraryItemQuery, GetLibraryItemScansQuery } from "graphql/generated";
type SortRangedContentProps =
| NonNullable<
@ -26,25 +21,6 @@ export const sortRangedContent = (contents: SortRangedContentProps): void => {
});
};
export const getStatusDescription = (status: string, langui: Langui): string | null | undefined => {
switch (status) {
case Enum_Componentsetstextset_Status.Incomplete:
return langui.status_incomplete;
case Enum_Componentsetstextset_Status.Draft:
return langui.status_draft;
case Enum_Componentsetstextset_Status.Review:
return langui.status_review;
case Enum_Componentsetstextset_Status.Done:
return langui.status_done;
default:
return "";
}
};
export const iterateMap = <K, V, U>(
map: Map<K, V>,
callbackfn: (key: K, value: V, index: number) => U,

90
src/hooks/useFormat.ts Normal file
View File

@ -0,0 +1,90 @@
import { IntlMessageFormat } from "intl-messageformat";
import { useCallback } from "react";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { LibraryItemMetadataDynamicZone } from "graphql/generated";
import { ICUParams } from "graphql/icuParams";
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
type WordingKey = keyof ICUParams;
type LibraryItemType = Exclude<LibraryItemMetadataDynamicZone["__typename"], undefined>;
export type ContentStatus = "Done" | "Draft" | "Incomplete" | "Review";
const componentMetadataToWording: Record<LibraryItemType, WordingKey> = {
ComponentMetadataAudio: "audio",
ComponentMetadataBooks: "textual",
ComponentMetadataGame: "game",
ComponentMetadataGroup: "group",
ComponentMetadataVideo: "video",
ComponentMetadataOther: "other",
Error: "item",
};
const componentSetsTextsetStatusToWording: Record<
ContentStatus,
{ label: WordingKey; description: WordingKey }
> = {
Draft: { label: "draft", description: "status_draft" },
Incomplete: { label: "incomplete", description: "status_incomplete" },
Review: { label: "review", description: "status_review" },
Done: { label: "done", description: "status_done" },
};
export const useFormat = (): {
format: <K extends WordingKey>(
key: K,
...values: ICUParams[K] extends never ? [undefined?] : [ICUParams[K]]
) => string;
formatLibraryItemType: (metadata: { __typename: LibraryItemType }) => string;
formatStatusLabel: (status: ContentStatus) => string;
formatStatusDescription: (status: ContentStatus) => string;
} => {
const langui = useAtomGetter(atoms.localData.langui);
const fallbackLangui = useAtomGetter(atoms.localData.fallbackLangui);
const format = useCallback(
(
key: WordingKey,
values?: Record<string, Date | boolean | number | string | null | undefined>
): string => {
const processedValues = Object.fromEntries(
Object.entries(values ?? {}).map(([oKey, value]) => [
oKey,
isDefined(value) ? value : "undefined",
])
);
const result = new IntlMessageFormat(langui[key] ?? "").format(processedValues).toString();
if (isDefinedAndNotEmpty(result)) {
return result;
}
return new IntlMessageFormat(fallbackLangui[key] ?? "").format(processedValues).toString();
},
[langui, fallbackLangui]
);
const formatLibraryItemType = useCallback(
(metadata: { __typename: LibraryItemType }): string =>
format(componentMetadataToWording[metadata.__typename]),
[format]
);
const formatStatusLabel = useCallback(
(status: ContentStatus): string => format(componentSetsTextsetStatusToWording[status].label),
[format]
);
const formatStatusDescription = useCallback(
(status: ContentStatus): string =>
format(componentSetsTextsetStatusToWording[status].description),
[format]
);
return {
format,
formatLibraryItemType,
formatStatusLabel,
formatStatusDescription,
};
};

View File

@ -3,10 +3,9 @@ import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel } from "components/Containers/ContentPanel";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { Img } from "components/Img";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -16,7 +15,7 @@ import { useAtomGetter } from "helpers/atoms";
interface Props extends AppLayoutRequired {}
const FourOhFour = ({ openGraph, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<AppLayout
contentPanel={
@ -26,7 +25,7 @@ const FourOhFour = ({ openGraph, ...otherProps }: Props): JSX.Element => {
className="animate-zoom-in drop-shadow-lg shadow-shade"
/>
<div className="mt-8 grid place-items-center gap-6">
<h2>{langui.page_not_found}</h2>
<h2>{format("page_not_found")}</h2>
<ReturnButton href="/" title="Home" />
</div>
</ContentPanel>
@ -44,9 +43,9 @@ export default FourOhFour;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, `404 - ${langui.page_not_found}`),
openGraph: getOpenGraph(format, `404 - ${format("page_not_found")}`),
};
return {
props: props,

View File

@ -3,10 +3,9 @@ import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel } from "components/Containers/ContentPanel";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { Img } from "components/Img";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -16,7 +15,7 @@ import { useAtomGetter } from "helpers/atoms";
interface Props extends AppLayoutRequired {}
const FiveHundred = ({ openGraph, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<AppLayout
contentPanel={
@ -26,7 +25,7 @@ const FiveHundred = ({ openGraph, ...otherProps }: Props): JSX.Element => {
className="animate-zoom-in drop-shadow-lg shadow-shade"
/>
<div className="mt-8 grid place-items-center gap-6">
<h2>{langui.page_not_found}</h2>
<h2>{format("page_not_found")}</h2>
<ReturnButton href="/" title="Home" />
</div>
</ContentPanel>
@ -44,9 +43,10 @@ export default FiveHundred;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, "500 - Internal Server Error"),
/* TODO: Langui */
openGraph: getOpenGraph(format, "500 - Internal Server Error"),
};
return {
props: props,

View File

@ -1,7 +1,6 @@
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -9,12 +8,12 @@ import { useAtomGetter } from "helpers/atoms";
*/
const AccordsHandbook = (props: PostStaticProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<PostPage
{...props}
returnHref="/about-us/"
returnTitle={langui.about_us}
returnTitle={format("about_us")}
displayToc
displayLanguageSwitcher
/>

View File

@ -9,6 +9,7 @@ import { RequestMailProps, ResponseMailProps } from "pages/api/mail";
import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -17,7 +18,7 @@ import { useAtomGetter } from "helpers/atoms";
const AboutUs = (props: PostStaticProps): JSX.Element => {
const router = useRouter();
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const [formResponse, setFormResponse] = useState("");
const [formState, setFormState] = useState<"completed" | "ongoing" | "stale">("stale");
@ -65,13 +66,13 @@ const AboutUs = (props: PostStaticProps): JSX.Element => {
.then((response: ResponseMailProps) => {
switch (response.code) {
case "OKAY":
setFormResponse(langui.response_email_success ?? "");
setFormResponse(format("response_email_success"));
setFormState("completed");
sendAnalytics("Contact", "Send email (success)");
break;
case "EENVELOPE":
setFormResponse(langui.response_invalid_email ?? "");
setFormResponse(format("response_invalid_email"));
setFormState("stale");
sendAnalytics("Contact", "Send email (invalid email)");
break;
@ -84,7 +85,7 @@ const AboutUs = (props: PostStaticProps): JSX.Element => {
}
});
} else {
setFormResponse(langui.response_invalid_code ?? "");
setFormResponse(format("response_invalid_code"));
setFormState("stale");
setRandomNumber1(randomInt(0, 10));
setRandomNumber2(randomInt(0, 10));
@ -94,7 +95,7 @@ const AboutUs = (props: PostStaticProps): JSX.Element => {
fields.verif.value = "";
}}>
<div className="flex flex-col place-items-center gap-1">
<label htmlFor="name">{langui.name}:</label>
<label htmlFor="name">{format("name")}:</label>
<input
type="text"
className={cIf(is1ColumnLayout, "w-full")}
@ -106,7 +107,7 @@ const AboutUs = (props: PostStaticProps): JSX.Element => {
</div>
<div className="flex flex-col place-items-center gap-1">
<label htmlFor="email">{langui.email}:</label>
<label htmlFor="email">{format("email")}:</label>
<input
type="email"
className={cIf(is1ColumnLayout, "w-full")}
@ -115,11 +116,11 @@ const AboutUs = (props: PostStaticProps): JSX.Element => {
required
disabled={formState !== "stale"}
/>
<p className="text-sm italic text-dark opacity-70">{langui.email_gdpr_notice}</p>
<p className="text-sm italic text-dark opacity-70">{format("email_gdpr_notice")}</p>
</div>
<div className="flex w-full flex-col place-items-center gap-1">
<label htmlFor="message">{langui.message}:</label>
<label htmlFor="message">{format("message")}:</label>
<textarea
name="message"
id="message"
@ -147,7 +148,7 @@ const AboutUs = (props: PostStaticProps): JSX.Element => {
<input
type="submit"
value={langui.send ?? "Send"}
value={format("send")}
className="w-min !px-6"
disabled={formState !== "stale"}
/>
@ -168,7 +169,7 @@ const AboutUs = (props: PostStaticProps): JSX.Element => {
<PostPage
{...props}
returnHref="/about-us/"
returnTitle={langui.about_us}
returnTitle={format("about_us")}
displayToc
appendBody={contactForm}
displayLanguageSwitcher

View File

@ -5,9 +5,8 @@ import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Containers/SubPanel";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -17,23 +16,23 @@ import { useAtomGetter } from "helpers/atoms";
interface Props extends AppLayoutRequired {}
const AboutUs = (props: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<AppLayout
subPanel={
<SubPanel>
<PanelHeader
icon="info"
title={langui.about_us}
description={langui.about_us_description}
title={format("about_us")}
description={format("about_us_description")}
/>
<HorizontalLine />
<NavOption title={langui.accords_handbook} url="/about-us/accords-handbook" border />
<NavOption title={langui.legality} url="/about-us/legality" border />
<NavOption title={langui.sharing_policy} url="/about-us/sharing-policy" border />
<NavOption title={langui.contact_us} url="/about-us/contact" border />
<NavOption title={format("accords_handbook")} url="/about-us/accords-handbook" border />
<NavOption title={format("legality")} url="/about-us/legality" border />
<NavOption title={format("sharing_policy")} url="/about-us/sharing-policy" border />
<NavOption title={format("contact_us")} url="/about-us/contact" border />
</SubPanel>
}
{...props}
@ -48,9 +47,9 @@ export default AboutUs;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.about_us ?? "About us"),
openGraph: getOpenGraph(format, format("about_us")),
};
return {
props: props,

View File

@ -1,7 +1,6 @@
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -9,12 +8,12 @@ import { useAtomGetter } from "helpers/atoms";
*/
const Legality = (props: PostStaticProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<PostPage
{...props}
returnHref="/about-us/"
returnTitle={langui.about_us}
returnTitle={format("about_us")}
displayToc
displayLanguageSwitcher
/>

View File

@ -1,7 +1,6 @@
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
@ -9,12 +8,12 @@ import { useAtomGetter } from "helpers/atoms";
*/
const SharingPolicy = (props: PostStaticProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<PostPage
{...props}
returnHref="/about-us/"
returnTitle={langui.about_us}
returnTitle={format("about_us")}
displayToc
displayLanguageSwitcher
/>

View File

@ -5,9 +5,8 @@ import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Containers/SubPanel";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -17,10 +16,14 @@ import { useAtomGetter } from "helpers/atoms";
interface Props extends AppLayoutRequired {}
const Archives = (props: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const subPanel = (
<SubPanel>
<PanelHeader icon="save" title={langui.archives} description={langui.archives_description} />
<PanelHeader
icon="save"
title={format("archives")}
description={format("archives_description")}
/>
<HorizontalLine />
<NavOption title={"Videos"} url="/archives/videos/" border />
</SubPanel>
@ -36,9 +39,9 @@ export default Archives;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.archives ?? "Archives"),
openGraph: getOpenGraph(format, format("archives")),
};
return {
props: props,

View File

@ -13,9 +13,6 @@ import { SubPanel } from "components/Containers/SubPanel";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { CustomSearchResponse, meiliSearch } from "helpers/search";
import { MeiliIndices, MeiliVideo } from "shared/meilisearch-graphql-typings/meiliTypes";
import { PreviewCard } from "components/PreviewCard";
@ -28,6 +25,8 @@ import { Button } from "components/Inputs/Button";
import { GetVideoChannelQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { Paginator } from "components/Containers/Paginator";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -61,27 +60,20 @@ interface Props extends AppLayoutRequired {
}
const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const hoverable = useDeviceSupportsHover();
const router = useTypedRouter(queryParamSchema);
const sortingMethods = useMemo(
() => [
{ meiliAttribute: "sortable_published_date:asc", displayedName: langui.oldest },
{ meiliAttribute: "sortable_published_date:desc", displayedName: langui.newest },
{ meiliAttribute: "views:asc", displayedName: langui.least_popular },
{ meiliAttribute: "views:desc", displayedName: langui.most_popular },
{ meiliAttribute: "duration:asc", displayedName: langui.shortest },
{ meiliAttribute: "duration:desc", displayedName: langui.longest },
{ meiliAttribute: "sortable_published_date:asc", displayedName: format("oldest") },
{ meiliAttribute: "sortable_published_date:desc", displayedName: format("newest") },
{ meiliAttribute: "views:asc", displayedName: format("least_popular") },
{ meiliAttribute: "views:desc", displayedName: format("most_popular") },
{ meiliAttribute: "duration:asc", displayedName: format("shortest") },
{ meiliAttribute: "duration:desc", displayedName: format("longest") },
],
[
langui.least_popular,
langui.longest,
langui.most_popular,
langui.newest,
langui.oldest,
langui.shortest,
]
[format]
);
const {
@ -159,7 +151,7 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
<SubPanel>
<ReturnButton
href="/archives/videos"
title={langui.videos}
title={format("videos")}
displayOnlyOn={"3ColumnsLayout"}
className="mb-10"
/>
@ -167,14 +159,16 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
<PanelHeader
icon="movie"
title={channel.title}
description={`${channel.subscribers.toLocaleString()} ${langui.subscribers?.toLowerCase()}`}
description={`${channel.subscribers.toLocaleString()} ${format(
"subscribers"
).toLowerCase()}`}
/>
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title}
placeholder={format("search_title")}
value={query}
onChange={(newQuery) => {
setPage(1);
@ -187,10 +181,10 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
}}
/>
<WithLabel label={langui.order_by}>
<WithLabel label={format("order_by")}>
<Select
className="w-full"
options={sortingMethods.map((item) => item.displayedName ?? "")}
options={sortingMethods.map((item) => item.displayedName)}
value={sortingMethod}
onChange={(newSort) => {
setPage(1);
@ -203,7 +197,7 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
/>
</WithLabel>
<WithLabel label={langui.only_unavailable_videos}>
<WithLabel label={format("only_unavailable_videos")}>
<Switch
value={onlyShowGone}
onClick={() => {
@ -213,14 +207,14 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
</WithLabel>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<WithLabel label={format("always_show_info")}>
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
</WithLabel>
)}
<Button
className="mt-8"
text={langui.reset_all_filters}
text={format("reset_all_filters")}
icon="settings_backup_restore"
onClick={() => {
setOnlyShowGone(DEFAULT_FILTERS_STATE.onlyShowGone);
@ -281,7 +275,7 @@ export default Channel;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const channel = await sdk.getVideoChannel({
channel: context.params && isDefined(context.params.uid) ? context.params.uid.toString() : "",
});
@ -289,7 +283,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
const props: Props = {
channel: channel.videoChannels.data[0].attributes,
openGraph: getOpenGraph(langui, channel.videoChannels.data[0].attributes.title),
openGraph: getOpenGraph(format, channel.videoChannels.data[0].attributes.title),
};
return {
props: props,

View File

@ -13,9 +13,6 @@ import { SubPanel } from "components/Containers/SubPanel";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { CustomSearchResponse, meiliSearch } from "helpers/search";
import { MeiliIndices, MeiliVideo } from "shared/meilisearch-graphql-typings/meiliTypes";
import { PreviewCard } from "components/PreviewCard";
@ -26,6 +23,8 @@ import { Select } from "components/Inputs/Select";
import { sendAnalytics } from "helpers/analytics";
import { Button } from "components/Inputs/Button";
import { Paginator } from "components/Containers/Paginator";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -55,27 +54,20 @@ const queryParamSchema = z.object({
interface Props extends AppLayoutRequired {}
const Videos = ({ ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const hoverable = useDeviceSupportsHover();
const router = useTypedRouter(queryParamSchema);
const sortingMethods = useMemo(
() => [
{ meiliAttribute: "sortable_published_date:asc", displayedName: langui.oldest },
{ meiliAttribute: "sortable_published_date:desc", displayedName: langui.newest },
{ meiliAttribute: "views:asc", displayedName: langui.least_popular },
{ meiliAttribute: "views:desc", displayedName: langui.most_popular },
{ meiliAttribute: "duration:asc", displayedName: langui.shortest },
{ meiliAttribute: "duration:desc", displayedName: langui.longest },
{ meiliAttribute: "sortable_published_date:asc", displayedName: format("oldest") },
{ meiliAttribute: "sortable_published_date:desc", displayedName: format("newest") },
{ meiliAttribute: "views:asc", displayedName: format("least_popular") },
{ meiliAttribute: "views:desc", displayedName: format("most_popular") },
{ meiliAttribute: "duration:asc", displayedName: format("shortest") },
{ meiliAttribute: "duration:desc", displayedName: format("longest") },
],
[
langui.least_popular,
langui.longest,
langui.most_popular,
langui.newest,
langui.oldest,
langui.shortest,
]
[format]
);
const {
@ -158,13 +150,17 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
className="mb-10"
/>
<PanelHeader icon="movie" title={langui.videos} description={langui.archives_description} />
<PanelHeader
icon="movie"
title={format("videos")}
description={format("archives_description")}
/>
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title}
placeholder={format("search_title")}
value={query}
onChange={(newQuery) => {
setPage(1);
@ -177,10 +173,10 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
}}
/>
<WithLabel label={langui.order_by}>
<WithLabel label={format("order_by")}>
<Select
className="w-full"
options={sortingMethods.map((item) => item.displayedName ?? "")}
options={sortingMethods.map((item) => item.displayedName)}
value={sortingMethod}
onChange={(newSort) => {
setPage(1);
@ -193,7 +189,7 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
/>
</WithLabel>
<WithLabel label={langui.only_unavailable_videos}>
<WithLabel label={format("only_unavailable_videos")}>
<Switch
value={onlyShowGone}
onClick={() => {
@ -203,14 +199,14 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => {
</WithLabel>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<WithLabel label={format("always_show_info")}>
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
</WithLabel>
)}
<Button
className="mt-8"
text={langui.reset_all_filters}
text={format("reset_all_filters")}
icon="settings_backup_restore"
onClick={() => {
setPage(1);
@ -271,9 +267,9 @@ export default Videos;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.videos ?? "Videos"),
openGraph: getOpenGraph(format, format("videos")),
};
return {
props: props,

View File

@ -15,10 +15,11 @@ import { prettyDate, prettyShortenNumber } from "helpers/formatters";
import { filterHasAttributes, isDefined } from "helpers/asserts";
import { getVideoFile } from "helpers/videos";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { Link } from "components/Inputs/Link";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -31,22 +32,22 @@ interface Props extends AppLayoutRequired {
const Video = ({ video, ...otherProps }: Props): JSX.Element => {
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const router = useRouter();
const subPanel = (
<SubPanel>
<ReturnButton
href="/archives/videos/"
title={langui.videos}
title={format("videos")}
displayOnlyOn={"3ColumnsLayout"}
/>
<HorizontalLine />
<NavOption title={langui.video} url="#video" border />
<NavOption title={langui.channel} url="#channel" border />
<NavOption title={langui.description} url="#description" border />
<NavOption title={format("video")} url="#video" border />
<NavOption title={format("channel")} url="#channel" border />
<NavOption title={format("description")} url="#description" border />
</SubPanel>
);
@ -54,7 +55,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
<ContentPanel width={ContentPanelWidthSizes.Full}>
<ReturnButton
href="/library/"
title={langui.library}
title={format("library")}
displayOnlyOn={"1ColumnLayout"}
className="mb-10"
/>
@ -97,7 +98,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
</p>
)}
<Link href={`https://youtu.be/${video.uid}`} alwaysNewTab>
<Button size="small" text={`${langui.view_on} ${video.source}`} />
<Button size="small" text={`${format("view_on")} ${video.source}`} />
</Link>
</div>
</div>
@ -106,7 +107,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
{video.channel?.data?.attributes && (
<InsetBox id="channel" className="grid place-items-center">
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-4 text-center">
<h2 className="text-2xl">{langui.channel}</h2>
<h2 className="text-2xl">{format("channel")}</h2>
<div>
<Button
href={`/archives/videos/c/${video.channel.data.attributes.uid}\
@ -115,7 +116,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
/>
<p>
{`${video.channel.data.attributes.subscribers.toLocaleString()}
${langui.subscribers?.toLowerCase()}`}
${format("subscribers").toLowerCase()}`}
</p>
</div>
</div>
@ -124,7 +125,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
<InsetBox id="description" className="grid place-items-center">
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
<h2 className="text-2xl">{langui.description}</h2>
<h2 className="text-2xl">{format("description")}</h2>
<p className="whitespace-pre-line">{video.description}</p>
</div>
</InsetBox>
@ -143,7 +144,7 @@ export default Video;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const videos = await sdk.getVideo({
uid: context.params && isDefined(context.params.uid) ? context.params.uid.toString() : "",
});
@ -151,7 +152,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
const props: Props = {
video: videos.videos.data[0].attributes,
openGraph: getOpenGraph(langui, videos.videos.data[0].attributes.title),
openGraph: getOpenGraph(format, videos.videos.data[0].attributes.title),
};
return {
props: props,

View File

@ -17,11 +17,10 @@ import { getOpenGraph } from "helpers/openGraph";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
import { getDescription } from "helpers/description";
import { TranslatedChroniclesList } from "components/Chronicles/ChroniclesList";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { Ids } from "types/ids";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -34,7 +33,7 @@ interface Props extends AppLayoutRequired {
}
const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
useScrollTopOnChange(Ids.ContentPanel, [chronicle.slug]);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
@ -71,7 +70,7 @@ const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element =
<ReturnButton
displayOnlyOn={"1ColumnLayout"}
href="/chronicles"
title={langui.chronicles}
title={format("chronicles")}
className="mb-10"
/>
@ -119,7 +118,11 @@ const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element =
const subPanel = (
<SubPanel>
<ReturnButton displayOnlyOn={"3ColumnsLayout"} href="/chronicles" title={langui.chronicles} />
<ReturnButton
displayOnlyOn={"3ColumnsLayout"}
href="/chronicles"
title={format("chronicles")}
/>
<HorizontalLine />
@ -160,7 +163,7 @@ export default Chronicle;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const slug =
context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "";
const chronicle = await sdk.getChronicle({
@ -190,11 +193,11 @@ export const getStaticProps: GetStaticProps = async (context) => {
selectedContentTranslation.subtitle
),
description: getDescription(selectedContentTranslation.description, {
[langui.type ?? "Type"]: [
[format("type", { count: Infinity })]: [
chronicle.chronicles.data[0].attributes.contents.data[0].attributes.type?.data
?.attributes?.titles?.[0]?.title,
],
[langui.categories ?? "Categories"]: filterHasAttributes(
[format("category", { count: Infinity })]: filterHasAttributes(
chronicle.chronicles.data[0].attributes.contents.data[0].attributes.categories
?.data,
["attributes"] as const
@ -231,7 +234,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
const props: Props = {
chronicle: chronicle.chronicles.data[0].attributes as ChronicleWithTranslations,
chapters: chronicles.chroniclesChapters.data,
openGraph: getOpenGraph(langui, title, description, thumbnail),
openGraph: getOpenGraph(format, title, description, thumbnail),
};
return {
props: props,

View File

@ -9,9 +9,8 @@ import { prettySlug } from "helpers/formatters";
import { getOpenGraph } from "helpers/openGraph";
import { TranslatedChroniclesList } from "components/Chronicles/ChroniclesList";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -23,13 +22,13 @@ interface Props extends AppLayoutRequired {
}
const Chronicles = ({ chapters, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const subPanel = (
<SubPanel>
<PanelHeader
icon="schedule"
title={langui.chronicles}
description={langui.chronicles_description}
title={format("chronicles")}
description={format("chronicles_description")}
/>
<HorizontalLine />
@ -63,13 +62,13 @@ export default Chronicles;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const chronicles = await sdk.getChroniclesChapters();
if (!chronicles.chroniclesChapters?.data) return { notFound: true };
const props: Props = {
chapters: chronicles.chroniclesChapters.data,
openGraph: getOpenGraph(langui, langui.chronicles ?? "Chronicles"),
openGraph: getOpenGraph(format, format("chronicles")),
};
return {
props: props,

View File

@ -21,7 +21,6 @@ import {
prettySlug,
} from "helpers/formatters";
import { isUntangibleGroupItem } from "helpers/libraryItem";
import { getStatusDescription } from "helpers/others";
import { filterHasAttributes, isDefinedAndNotEmpty } from "helpers/asserts";
import { ContentWithTranslations } from "types/types";
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
@ -31,10 +30,11 @@ import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/local
import { getDescription } from "helpers/description";
import { TranslatedPreviewLine } from "components/PreviewLine";
import { cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { Ids } from "types/ids";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -49,7 +49,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
const isContentPanelAtLeast2xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast2xl);
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const langui = useAtomGetter(atoms.localData.langui);
const { format, formatStatusDescription } = useFormat();
const languages = useAtomGetter(atoms.localData.languages);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
@ -86,9 +86,8 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
fallback: {
title: content.folder?.data?.attributes
? prettySlug(content.folder.data.attributes.slug)
: langui.contents,
: format("contents"),
},
langui,
};
const subPanel = (
@ -102,14 +101,14 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
<h2 className="text-xl">
{selectedTranslation.text_set.source_language.data.attributes.code ===
selectedTranslation.language?.data?.attributes?.code
? langui.transcript_notice
: langui.translation_notice}
? format("transcript_notice")
: format("translation_notice")}
</h2>
{selectedTranslation.text_set.source_language.data.attributes.code !==
selectedTranslation.language?.data?.attributes?.code && (
<div className="grid place-items-center gap-2">
<p className="font-headers font-bold">{langui.source_language}:</p>
<p className="font-headers font-bold">{format("source_language")}:</p>
<Chip
text={prettyLanguage(
selectedTranslation.text_set.source_language.data.attributes.code,
@ -120,10 +119,10 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
)}
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
<p className="font-headers font-bold">{langui.status}:</p>
<p className="font-headers font-bold">{format("status")}:</p>
<ToolTip
content={getStatusDescription(selectedTranslation.text_set.status, langui)}
content={formatStatusDescription(selectedTranslation.text_set.status)}
maxWidth={"20rem"}>
<Chip text={selectedTranslation.text_set.status} />
</ToolTip>
@ -132,7 +131,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
{selectedTranslation.text_set.transcribers &&
selectedTranslation.text_set.transcribers.data.length > 0 && (
<div>
<p className="font-headers font-bold">{langui.transcribers}:</p>
<p className="font-headers font-bold">{format("transcribers")}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedTranslation.text_set.transcribers.data, [
"attributes",
@ -149,7 +148,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
{selectedTranslation.text_set.translators &&
selectedTranslation.text_set.translators.data.length > 0 && (
<div>
<p className="font-headers font-bold">{langui.translators}:</p>
<p className="font-headers font-bold">{format("translators")}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedTranslation.text_set.translators.data, [
"attributes",
@ -166,7 +165,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
{selectedTranslation.text_set.proofreaders &&
selectedTranslation.text_set.proofreaders.data.length > 0 && (
<div>
<p className="font-headers font-bold">{langui.proofreaders}:</p>
<p className="font-headers font-bold">{format("proofreaders")}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedTranslation.text_set.proofreaders.data, [
"attributes",
@ -182,7 +181,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
<div>
<p className="font-headers font-bold">{langui.notes}:</p>
<p className="font-headers font-bold">{format("notes")}:</p>
<div className="grid place-content-center place-items-center gap-2">
<Markdawn text={selectedTranslation.text_set.notes} />
</div>
@ -210,7 +209,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
<>
<HorizontalLine />
<div>
<p className="font-headers text-2xl font-bold">{langui.source}</p>
<p className="font-headers text-2xl font-bold">{format("source")}</p>
<div className="mt-6 grid place-items-center gap-6">
{filterHasAttributes(content.ranged_contents.data, [
"attributes.library_item.data.attributes",
@ -283,7 +282,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
{previousContent?.attributes && (
<div className="mt-12 mb-8 w-full">
<h2 className="mb-4 text-center text-2xl">{langui.previous_content}</h2>
<h2 className="mb-4 text-center text-2xl">{format("previous_content")}</h2>
<TranslatedPreviewLine
href={`/contents/${previousContent.attributes.slug}`}
translations={filterHasAttributes(previousContent.attributes.translations, [
@ -328,7 +327,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => {
{nextContent?.attributes && (
<>
<HorizontalLine />
<h2 className="mb-4 text-center text-2xl">{langui.followup_content}</h2>
<h2 className="mb-4 text-center text-2xl">{format("followup_content")}</h2>
<TranslatedPreviewLine
href={`/contents/${nextContent.attributes.slug}`}
translations={filterHasAttributes(nextContent.attributes.translations, [
@ -375,7 +374,7 @@ export default Content;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const slug = context.params?.slug ? context.params.slug.toString() : "";
const content = await sdk.getContentText({
slug: slug,
@ -401,10 +400,10 @@ export const getStaticProps: GetStaticProps = async (context) => {
selectedTranslation.subtitle
),
description: getDescription(selectedTranslation.description, {
[langui.type ?? "Type"]: [
[format("type", { count: Infinity })]: [
content.contents.data[0].attributes.type?.data?.attributes?.titles?.[0]?.title,
],
[langui.categories ?? "Categories"]: filterHasAttributes(
[format("category", { count: Infinity })]: filterHasAttributes(
content.contents.data[0].attributes.categories?.data,
["attributes"] as const
).map((category) => category.attributes.short),
@ -426,7 +425,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
const props: Props = {
content: content.contents.data[0].attributes as ContentWithTranslations,
openGraph: getOpenGraph(langui, title, description, thumbnail),
openGraph: getOpenGraph(format, title, description, thumbnail),
};
return {
props: props,

View File

@ -20,16 +20,15 @@ import {
} from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search";
import { MeiliContent, MeiliIndices } from "shared/meilisearch-graphql-typings/meiliTypes";
import { useTypedRouter } from "hooks/useTypedRouter";
import { TranslatedPreviewCard } from "components/PreviewCard";
import { prettySlug } from "helpers/formatters";
import { Paginator } from "components/Containers/Paginator";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -58,16 +57,16 @@ interface Props extends AppLayoutRequired {}
const Contents = (props: Props): JSX.Element => {
const hoverable = useDeviceSupportsHover();
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const router = useTypedRouter(queryParamSchema);
const sortingMethods = useMemo(
() => [
{ meiliAttribute: "slug:asc", displayedName: langui.name },
{ meiliAttribute: "sortable_updated_date:asc", displayedName: langui.oldest },
{ meiliAttribute: "sortable_updated_date:desc", displayedName: langui.newest },
{ meiliAttribute: "slug:asc", displayedName: format("name") },
{ meiliAttribute: "sortable_updated_date:asc", displayedName: format("oldest") },
{ meiliAttribute: "sortable_updated_date:desc", displayedName: format("newest") },
],
[langui.name, langui.newest, langui.oldest]
[format]
);
const [sortingMethod, setSortingMethod] = useState<number>(
@ -131,19 +130,19 @@ const Contents = (props: Props): JSX.Element => {
<SubPanel>
<PanelHeader
icon="workspaces"
title={langui.contents}
description={langui.contents_description}
title={format("contents")}
description={format("contents_description")}
/>
<HorizontalLine />
<Button href="/contents" text={langui.switch_to_folder_view} icon="folder" />
<Button href="/contents" text={format("switch_to_folder_view")} icon="folder" />
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."}
placeholder={format("search_title")}
value={query}
onChange={(name) => {
setPage(1);
@ -156,10 +155,10 @@ const Contents = (props: Props): JSX.Element => {
}}
/>
<WithLabel label={langui.order_by}>
<WithLabel label={format("order_by")}>
<Select
className="w-full"
options={sortingMethods.map((item) => item.displayedName ?? "")}
options={sortingMethods.map((item) => item.displayedName)}
value={sortingMethod}
onChange={(newSort) => {
setPage(1);
@ -173,7 +172,7 @@ const Contents = (props: Props): JSX.Element => {
</WithLabel>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<WithLabel label={format("always_show_info")}>
<Switch
value={keepInfoVisible}
onClick={() => {
@ -186,7 +185,7 @@ const Contents = (props: Props): JSX.Element => {
<Button
className="mt-8"
text={langui.reset_all_filters}
text={format("reset_all_filters")}
icon="settings_backup_restore"
onClick={() => {
setPage(1);
@ -254,10 +253,10 @@ export default Contents;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.contents ?? "Contents"),
openGraph: getOpenGraph(format, format("contents")),
};
return {
props: props,

View File

@ -16,10 +16,11 @@ import { SubPanel } from "components/Containers/SubPanel";
import { TranslatedPreviewCard } from "components/PreviewCard";
import { HorizontalLine } from "components/HorizontalLine";
import { cJoin, cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { TranslatedPreviewFolder } from "components/Contents/PreviewFolder";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -33,20 +34,20 @@ interface Props extends AppLayoutRequired {
}
const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const subPanel = (
<SubPanel>
<PanelHeader
icon="workspaces"
title={langui.contents}
description={langui.contents_description}
title={format("contents")}
description={format("contents_description")}
/>
<HorizontalLine />
<Button href="/contents/all" text={langui.switch_to_grid_view} icon="apps" />
<Button href="/contents/all" text={format("switch_to_grid_view")} icon="apps" />
</SubPanel>
);
@ -117,7 +118,7 @@ const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Elemen
)
)}
renderWhenEmpty={() => <></>}
groupingFunction={() => [langui.folders ?? "Folders"]}
groupingFunction={() => [format("folders")]}
/>
<SmartList
@ -159,7 +160,7 @@ const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Elemen
"grid-cols-2 gap-x-3 gap-y-5"
)}
renderWhenEmpty={() => <></>}
groupingFunction={() => [langui.contents ?? "Contents"]}
groupingFunction={() => [format("contents")]}
/>
{folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && (
@ -186,7 +187,7 @@ export default ContentsFolder;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const slug = context.params?.slug ? context.params.slug.toString() : "";
const contentsFolder = await sdk.getContentsFolder({
slug: slug,
@ -208,7 +209,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
const title = (() => {
if (slug === "root") {
return langui.contents ?? "Contents";
return format("contents");
}
if (context.locale && context.locales) {
const selectedTranslation = staticSmartLanguage({
@ -224,7 +225,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
})();
const props: Props = {
openGraph: getOpenGraph(langui, title),
openGraph: getOpenGraph(format, title),
folder,
};
return {
@ -258,13 +259,13 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
*/
const NoContentNorFolderMessage = () => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
return (
<div className="grid place-content-center">
<div
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
border-dark p-8 text-dark opacity-40">
<p className="max-w-xs text-2xl">{langui.empty_folder_message}</p>
<p className="max-w-xs text-2xl">{format("empty_folder_message")}</p>
</div>
</div>
);

View File

@ -9,8 +9,8 @@ import { getReadySdk } from "graphql/sdk";
import { filterDefined, filterHasAttributes } from "helpers/asserts";
import { Report, Severity } from "types/Report";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { sJoin } from "helpers/formatters";
import { getFormat } from "helpers/i18n";
/*
*
@ -83,11 +83,11 @@ export default CheckupContents;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const contents = await sdk.devGetContents();
const props: Props = {
contents: contents,
openGraph: getOpenGraph(langui, "Checkup Contents"),
openGraph: getOpenGraph(format, "Checkup Contents"),
};
return {
props: props,

View File

@ -11,8 +11,8 @@ import {
import { getReadySdk } from "graphql/sdk";
import { Report, Severity } from "types/Report";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { sJoin } from "helpers/formatters";
import { getFormat } from "helpers/i18n";
/*
*
@ -85,12 +85,12 @@ export default CheckupLibraryItems;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const libraryItems = await sdk.devGetLibraryItems();
const props: Props = {
libraryItems: libraryItems,
openGraph: getOpenGraph(langui, "Checkup Library Items"),
openGraph: getOpenGraph(format, "Checkup Library Items"),
};
return {
props: props,

View File

@ -8,7 +8,7 @@ import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/Cont
import { Popup } from "components/Containers/Popup";
import { ToolTip } from "components/ToolTip";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { getFormat } from "helpers/i18n";
/*
*
@ -404,9 +404,9 @@ export default Editor;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, "Markdawn Editor"),
openGraph: getOpenGraph(format, "Markdawn Editor"),
};
return {
props: props,

View File

@ -3,7 +3,6 @@ import { GetStaticProps } from "next";
import { ReactNode, useState } from "react";
import Slider from "rc-slider";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { getLangui } from "graphql/fetchLocalData";
import { getOpenGraph } from "helpers/openGraph";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { Button } from "components/Inputs/Button";
@ -19,6 +18,7 @@ import { PreviewCard } from "components/PreviewCard";
import { PreviewLine } from "components/PreviewLine";
import { ChroniclePreview } from "components/Chronicles/ChroniclePreview";
import { PreviewFolder } from "components/Contents/PreviewFolder";
import { getFormat } from "helpers/i18n";
/*
*
@ -880,9 +880,9 @@ export default DesignSystem;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, "Design System"),
openGraph: getOpenGraph(format, "Design System"),
};
return {
props: props,

View File

@ -6,7 +6,7 @@ import { ButtonGroup } from "components/Inputs/ButtonGroup";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { ToolTip } from "components/ToolTip";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { getFormat } from "helpers/i18n";
/*
*
@ -533,9 +533,9 @@ export default Transcript;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, "Japanese Transcription Tool"),
openGraph: getOpenGraph(format, "Japanese Transcription Tool"),
};
return {
props: props,

View File

@ -1,17 +1,22 @@
import { GetStaticProps } from "next";
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { getOpenGraph } from "helpers/openGraph";
import { PostStaticProps } from "graphql/getPostStaticProps";
import { Terminal } from "components/Cli/Terminal";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { getOpenGraph } from "helpers/openGraph";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
import { getReadySdk } from "graphql/sdk";
import { PostWithTranslations } from "types/types";
/*
*
* PAGE
*/
const Home = ({ ...otherProps }: PostStaticProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const Home = (props: PostStaticProps): JSX.Element => {
const { format } = useFormat();
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
if (isTerminalMode) {
@ -34,7 +39,7 @@ const Home = ({ ...otherProps }: PostStaticProps): JSX.Element => {
return (
<PostPage
{...otherProps}
{...props}
prependBody={
<div className="grid w-full place-content-center place-items-center gap-5 text-center">
<div
@ -46,7 +51,6 @@ const Home = ({ ...otherProps }: PostStaticProps): JSX.Element => {
</div>
}
displayTitle={false}
openGraph={getOpenGraph(langui)}
displayLanguageSwitcher
/>
);
@ -59,4 +63,21 @@ export default Home;
* NEXT DATA FETCHING
*/
export const getStaticProps = getPostStaticProps("home");
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const { format } = getFormat(context.locale);
const post = await sdk.getPost({
slug: "home",
language_code: context.locale ?? "en",
});
if (post.posts?.data && post.posts.data.length > 0) {
const props: PostStaticProps = {
post: post.posts.data[0]?.attributes as PostWithTranslations,
openGraph: getOpenGraph(format),
};
return {
props: props,
};
}
return { notFound: true };
};

View File

@ -24,7 +24,6 @@ import {
prettyDate,
prettyInlineTitle,
prettyItemSubType,
prettyItemType,
prettyPrice,
prettySlug,
prettyURL,
@ -49,11 +48,12 @@ import { getOpenGraph } from "helpers/openGraph";
import { getDescription } from "helpers/description";
import { useIntersectionList } from "hooks/useIntersectionList";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { Ids } from "types/ids";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { Link } from "components/Inputs/Link";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -74,7 +74,7 @@ interface Props extends AppLayoutRequired {
const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
const currency = useAtomGetter(atoms.settings.currency);
const langui = useAtomGetter(atoms.localData.langui);
const { format, formatLibraryItemType } = useFormat();
const currencies = useAtomGetter(atoms.localData.currencies);
const isContentPanelAtLeast3xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast3xl);
@ -99,13 +99,13 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
const subPanel = (
<SubPanel>
<ReturnButton href="/library/" title={langui.library} displayOnlyOn="3ColumnsLayout" />
<ReturnButton href="/library/" title={format("library")} displayOnlyOn="3ColumnsLayout" />
<HorizontalLine />
<div className="grid gap-4">
<NavOption
title={langui.summary}
title={format("summary")}
url={`#${intersectionIds[0]}`}
border
active={currentIntersection === 0}
@ -113,7 +113,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
{item.gallery && item.gallery.data.length > 0 && (
<NavOption
title={langui.gallery}
title={format("gallery")}
url={`#${intersectionIds[1]}`}
border
active={currentIntersection === 1}
@ -121,7 +121,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
)}
<NavOption
title={langui.details}
title={format("details")}
url={`#${intersectionIds[2]}`}
border
active={currentIntersection === 2}
@ -129,7 +129,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
{item.subitems && item.subitems.data.length > 0 && (
<NavOption
title={isVariantSet ? langui.variants : langui.subitems}
title={format(isVariantSet ? "variant" : "subitem", { count: Infinity })}
url={`#${intersectionIds[3]}`}
border
active={currentIntersection === 3}
@ -138,7 +138,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
{item.contents && item.contents.data.length > 0 && (
<NavOption
title={langui.contents}
title={format("contents")}
url={`#${intersectionIds[4]}`}
border
active={currentIntersection === 4}
@ -152,7 +152,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
<ContentPanel width={ContentPanelWidthSizes.Full}>
<ReturnButton
href="/library/"
title={langui.library}
title={format("library")}
displayOnlyOn="1ColumnLayout"
className="mb-10"
/>
@ -180,7 +180,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
{item.subitem_of?.data[0]?.attributes && (
<div className="grid place-items-center">
<p>{langui.subitem_of}</p>
<p>{format("subitem_of_x", { x: "" })}</p>
<Button
href={`/library/${item.subitem_of.data[0].attributes.slug}`}
text={prettyInlineTitle(
@ -212,7 +212,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
<>
{item.urls?.length ? (
<div className="flex flex-row place-items-center gap-3">
<p>{langui.available_at}</p>
<p>{format("available_at")}</p>
{filterHasAttributes(item.urls, ["url"] as const).map((url, index) => (
<Fragment key={index}>
<Button href={url.url} text={prettyURL(url.url)} alwaysNewTab />
@ -220,7 +220,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
))}
</div>
) : (
<p>{langui.item_not_available}</p>
<p>{format("item_not_available")}</p>
)}
</>
)}
@ -229,7 +229,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
{item.gallery && item.gallery.data.length > 0 && (
<div id={intersectionIds[1]} className="grid w-full place-items-center gap-8">
<h2 className="text-2xl">{langui.gallery}</h2>
<h2 className="text-2xl">{format("gallery")}</h2>
<div
className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end
gap-8">
@ -262,7 +262,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
<InsetBox id={intersectionIds[2]} className="grid place-items-center">
<div className="place-items grid w-[clamp(0px,100%,42rem)] gap-10">
<h2 className="text-center text-2xl">{langui.details}</h2>
<h2 className="text-center text-2xl">{format("details")}</h2>
<div
className={cJoin(
"grid place-items-center gap-y-8",
@ -270,9 +270,9 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
)}>
{item.metadata?.[0] && (
<div className="grid place-content-start place-items-center">
<h3 className="text-xl">{langui.type}</h3>
<h3 className="text-xl">{format("type", { count: 1 })}</h3>
<div className="grid grid-flow-col gap-1">
<Chip text={prettyItemType(item.metadata[0], langui)} />
<Chip text={formatLibraryItemType(item.metadata[0])} />
{""}
<Chip text={prettyItemSubType(item.metadata[0])} />
</div>
@ -281,14 +281,14 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
{item.release_date && (
<div className="grid place-content-start place-items-center">
<h3 className="text-xl">{langui.release_date}</h3>
<h3 className="text-xl">{format("release_date")}</h3>
<p>{prettyDate(item.release_date, router.locale)}</p>
</div>
)}
{item.price && (
<div className="grid place-content-start place-items-center text-center">
<h3 className="text-xl">{langui.price}</h3>
<h3 className="text-xl">{format("price")}</h3>
<p>
{prettyPrice(
item.price,
@ -299,7 +299,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
{item.price.currency?.data?.attributes?.code !== currency && (
<p>
{prettyPrice(item.price, currencies, currency)} <br />(
{langui.calculated?.toLowerCase()})
{format("calculated").toLowerCase()})
</p>
)}
</div>
@ -308,7 +308,9 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
{item.categories && item.categories.data.length > 0 && (
<div className="flex flex-col place-items-center gap-2">
<h3 className="text-xl">{langui.categories}</h3>
<h3 className="text-xl">
{format("category", { count: item.categories.data.length })}
</h3>
<div className="flex flex-row flex-wrap place-content-center gap-2">
{filterHasAttributes(item.categories.data, ["attributes"] as const).map(
(category) => (
@ -325,7 +327,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
"grid gap-4",
cIf(!isContentPanelAtLeast3xl, "place-items-center")
)}>
<h3 className="text-xl">{langui.size}</h3>
<h3 className="text-xl">{format("size")}</h3>
<div
className={cJoin(
"grid w-full",
@ -344,7 +346,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
"place-items-center"
)
)}>
<p className="font-bold">{langui.width}:</p>
<p className="font-bold">{format("width")}:</p>
<div>
<p>{item.size.width} mm</p>
<p>{convertMmToInch(item.size.width)} in</p>
@ -359,7 +361,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
"place-items-center"
)
)}>
<p className="font-bold">{langui.height}:</p>
<p className="font-bold">{format("height")}:</p>
<div>
<p>{item.size.height} mm</p>
<p>{convertMmToInch(item.size.height)} in</p>
@ -375,7 +377,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
"place-items-center"
)
)}>
<p className="font-bold">{langui.thickness}:</p>
<p className="font-bold">{format("thickness")}:</p>
<div>
<p>{item.size.thickness} mm</p>
<p>{convertMmToInch(item.size.thickness)} in</p>
@ -393,44 +395,53 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
"grid gap-4",
cIf(!isContentPanelAtLeast3xl, "place-items-center")
)}>
<h3 className="text-xl">{langui.type_information}</h3>
<h3 className="text-xl">{format("type_information")}</h3>
<div className="flex flex-wrap place-content-between gap-x-8">
{item.metadata?.[0]?.__typename === "ComponentMetadataBooks" && (
<>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.pages}:</p>
<p>{item.metadata[0].page_count}</p>
</div>
{isDefined(item.metadata[0].page_count) && (
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{format("page", { count: Infinity })}:</p>
<p>{item.metadata[0].page_count}</p>
</div>
)}
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.binding}:</p>
<p className="font-bold">{format("binding")}:</p>
<p>
{item.metadata[0].binding_type ===
Enum_Componentmetadatabooks_Binding_Type.Paperback
? langui.paperback
? format("paperback")
: item.metadata[0].binding_type ===
Enum_Componentmetadatabooks_Binding_Type.Hardcover
? langui.hardcover
? format("hardcover")
: ""}
</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.page_order}:</p>
<p className="font-bold">{format("page_order")}:</p>
<p>
{item.metadata[0].page_order ===
Enum_Componentmetadatabooks_Page_Order.LeftToRight
? langui.left_to_right
: langui.right_to_left}
? format("left_to_right")
: format("right_to_left")}
</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.languages}:</p>
{item.metadata[0]?.languages?.data.map((lang) => (
<p key={lang.attributes?.code}>{lang.attributes?.name}</p>
))}
</div>
{isDefined(item.metadata[0].languages) && (
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">
{format("language", {
count: item.metadata[0].languages.data.length,
})}
:
</p>
{item.metadata[0].languages.data.map((lang) => (
<p key={lang.attributes?.code}>{lang.attributes?.name}</p>
))}
</div>
)}
</>
)}
</div>
@ -441,10 +452,12 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
{item.subitems && item.subitems.data.length > 0 && (
<div id={intersectionIds[3]} className="grid w-full place-items-center gap-8">
<h2 className="text-2xl">{isVariantSet ? langui.variants : langui.subitems}</h2>
<h2 className="text-2xl">
{format(isVariantSet ? "variant" : "subitem", { count: Infinity })}
</h2>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<WithLabel label={format("always_show_info")}>
<Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
</WithLabel>
)}
@ -493,10 +506,10 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
{item.contents && item.contents.data.length > 0 && (
<div id={intersectionIds[4]} className="grid w-full place-items-center gap-8">
<h2 className="-mb-6 text-2xl">{langui.contents}</h2>
<h2 className="-mb-6 text-2xl">{format("contents")}</h2>
{displayOpenScans && (
<div className="grid grid-flow-col gap-4">
<Button href={`/library/${item.slug}/reader`} text={langui.view_scans} />
<Button href={`/library/${item.slug}/reader`} text={format("view_scans")} />
</div>
)}
<div className="max-w- grid w-full gap-4">
@ -563,7 +576,7 @@ export default LibrarySlug;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const item = await sdk.getLibraryItem({
slug: context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "",
language_code: context.locale ?? "en",
@ -576,14 +589,14 @@ export const getStaticProps: GetStaticProps = async (context) => {
const description = getDescription(
item.libraryItems.data[0].attributes.descriptions?.[0]?.description,
{
[langui.categories ?? "Categories"]: filterHasAttributes(
[format("category", { count: Infinity })]: filterHasAttributes(
item.libraryItems.data[0].attributes.categories?.data,
["attributes.short"]
).map((category) => category.attributes.short),
[langui.type ?? "Type"]: item.libraryItems.data[0].attributes.metadata?.[0]
[format("type", { count: Infinity })]: item.libraryItems.data[0].attributes.metadata?.[0]
? [prettyItemSubType(item.libraryItems.data[0].attributes.metadata[0])]
: [],
[langui.release_date ?? "Release date"]: [
[format("release_date")]: [
item.libraryItems.data[0].attributes.release_date
? prettyDate(item.libraryItems.data[0].attributes.release_date, context.locale)
: undefined,
@ -594,7 +607,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
const props: Props = {
item: item.libraryItems.data[0].attributes,
itemId: item.libraryItems.data[0].id,
openGraph: getOpenGraph(langui, title, description, thumbnail?.data?.attributes),
openGraph: getOpenGraph(format, title, description, thumbnail?.data?.attributes),
};
return {
props: props,
@ -651,7 +664,7 @@ const ContentLine = ({
parentSlug,
condensed,
}: ContentLineProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const { value: isOpened, toggle: toggleOpened } = useBoolean(false);
const [selectedTranslation] = useSmartLanguage({
items: content?.translations ?? [],
@ -692,15 +705,15 @@ const ContentLine = ({
{hasScanSet && (
<Button
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
text={langui.view_scans}
text={format("view_scans")}
/>
)}
{isDefined(content) && (
<Button href={`/contents/${content.slug}`} text={langui.open_content} />
<Button href={`/contents/${content.slug}`} text={format("open_content")} />
)}
</>
) : (
langui.content_is_not_available
format("content_is_not_available")
)}
</div>
</div>
@ -747,15 +760,15 @@ const ContentLine = ({
{hasScanSet && (
<Button
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
text={langui.view_scans}
text={format("view_scans")}
/>
)}
{isDefined(content) && (
<Button href={`/contents/${content.slug}`} text={langui.open_content} />
<Button href={`/contents/${content.slug}`} text={format("open_content")} />
)}
</>
) : (
langui.content_is_not_available
format("content_is_not_available")
)}
</div>
</div>

View File

@ -11,10 +11,9 @@ import {
UploadImageFragment,
} from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { getStatusDescription, sortRangedContent } from "helpers/others";
import { sortRangedContent } from "helpers/others";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { Img } from "components/Img";
import { getAssetFilename, ImageQuality } from "helpers/img";
@ -40,6 +39,8 @@ import { useAtomGetter } from "helpers/atoms";
import { FilterSettings, useReaderSettings } from "hooks/useReaderSettings";
import { useIsWebkit } from "hooks/useIsWebkit";
import { useTypedRouter } from "hooks/useTypedRouter";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
type BookType = "book" | "manga";
type DisplayMode = "double" | "single";
@ -96,7 +97,7 @@ const LibrarySlug = ({
...otherProps
}: Props): JSX.Element => {
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const isDarkMode = useAtomGetter(atoms.settings.darkMode);
const {
filterSettings,
@ -278,34 +279,34 @@ const LibrarySlug = ({
const subPanel = (
<SubPanel>
<ReturnButton title={langui.item} href={`/library/${itemSlug}`} />
<ReturnButton title={format("item", { count: Infinity })} href={`/library/${itemSlug}`} />
<div className="mt-4 grid gap-2">
<WithLabel label={langui.paper_texture}>
<WithLabel label={format("paper_texture")}>
<Switch value={filterSettings.paperTexture} onClick={togglePaperTexture} />
</WithLabel>
<WithLabel label={langui.book_fold}>
<WithLabel label={format("book_fold")}>
<Switch value={filterSettings.bookFold} onClick={toggleBookFold} />
</WithLabel>
<WithLabel label={langui.lighting}>
<WithLabel label={format("lighting")}>
<Switch value={filterSettings.lighting} onClick={toggleLighting} />
</WithLabel>
<WithLabel label={langui.side_pages}>
<WithLabel label={format("side_pages")}>
<Switch value={isSidePagesEnabled} onClick={toggleIsSidePagesEnabled} />
</WithLabel>
{!isWebkit && (
<WithLabel label={langui.shadow}>
<WithLabel label={format("shadow")}>
<Switch value={filterSettings.dropShadow} onClick={toggleDropShadow} />
</WithLabel>
)}
</div>
<div className="mt-4 grid">
<p>{langui.night_reader}:</p>
<p>{format("night_reader")}:</p>
<Slider
min={0}
max={10}
@ -318,18 +319,18 @@ const LibrarySlug = ({
</div>
<div className="mt-8 grid gap-2">
<p>{langui.reading_layout}:</p>
<p>{format("reading_layout")}:</p>
<ButtonGroup
buttonsProps={[
{
icon: "description",
tooltip: langui.single_page_view,
tooltip: format("single_page_view"),
active: displayMode === "single",
onClick: () => changeDisplayMode("single"),
},
{
icon: "auto_stories",
tooltip: langui.double_page_view,
tooltip: format("double_page_view"),
active: displayMode === "double",
onClick: () => changeDisplayMode("double"),
},
@ -338,7 +339,7 @@ const LibrarySlug = ({
</div>
<div className="mt-4 grid gap-2">
<p>{langui.quality}:</p>
<p>{format("quality")}:</p>
<ButtonGroup
buttonsProps={[
{
@ -357,7 +358,7 @@ const LibrarySlug = ({
<Button
className="mt-8"
text={langui.reset_all_options}
text={format("reset_all_options")}
icon="settings_backup_restore"
onClick={() => {
resetReaderSettings();
@ -576,7 +577,7 @@ export default LibrarySlug;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const item = await sdk.getLibraryItemScans({
slug: context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "",
language_code: context.locale ?? "en",
@ -657,7 +658,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
itemSlug: item.libraryItems.data[0].attributes.slug,
pageRatio: `${pages[0]?.width ?? 21} / ${pages[0]?.height ?? 29.7}`,
openGraph: getOpenGraph(
langui,
format,
item.libraryItems.data[0].attributes.title,
undefined,
item.libraryItems.data[0].attributes.thumbnail?.data?.attributes
@ -787,7 +788,7 @@ interface ScanSetProps {
const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps): JSX.Element => {
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const langui = useAtomGetter(atoms.localData.langui);
const { format, formatStatusDescription } = useFormat();
const [selectedScan, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: scanSet,
languageExtractor: useCallback(
@ -844,8 +845,8 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
text={
selectedScan.language?.data?.attributes?.code ===
selectedScan.source_language?.data?.attributes?.code
? langui.scan ?? "Scan"
: langui.scanlation ?? "Scanlation"
? format("scan")
: format("scanlation")
}
/>
</div>
@ -854,7 +855,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
{content?.data?.attributes && isDefinedAndNotEmpty(content.data.attributes.slug) && (
<Button
href={`/contents/${content.data.attributes.slug}`}
text={langui.open_content}
text={format("open_content")}
/>
)}
@ -863,17 +864,15 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
)}
<div className="grid place-content-center place-items-center">
<p className="font-headers font-bold">{langui.status}:</p>
<ToolTip
content={getStatusDescription(selectedScan.status, langui)}
maxWidth={"20rem"}>
<p className="font-headers font-bold">{format("status")}:</p>
<ToolTip content={formatStatusDescription(selectedScan.status)} maxWidth={"20rem"}>
<Chip text={selectedScan.status} />
</ToolTip>
</div>
{selectedScan.scanners && selectedScan.scanners.data.length > 0 && (
<div>
<p className="font-headers font-bold">{langui.scanners}:</p>
<p className="font-headers font-bold">{format("scanners")}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedScan.scanners.data, [
"id",
@ -889,7 +888,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
{selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && (
<div>
<p className="font-headers font-bold">{langui.cleaners}:</p>
<p className="font-headers font-bold">{format("cleaners")}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedScan.cleaners.data, [
"id",
@ -905,7 +904,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
{selectedScan.typesetters && selectedScan.typesetters.data.length > 0 && (
<div>
<p className="font-headers font-bold">{langui.typesetters}:</p>
<p className="font-headers font-bold">{format("typesetters")}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedScan.typesetters.data, [
"id",
@ -921,7 +920,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
{isDefinedAndNotEmpty(selectedScan.notes) && (
<ToolTip content={selectedScan.notes}>
<Chip text={langui.notes ?? "Notes"} />
<Chip text={format("notes")} />
</ToolTip>
)}
</div>

View File

@ -23,10 +23,7 @@ import {
} from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search";
import { MeiliIndices, MeiliLibraryItem } from "shared/meilisearch-graphql-typings/meiliTypes";
import { useTypedRouter } from "hooks/useTypedRouter";
@ -36,6 +33,8 @@ import { isUntangibleGroupItem } from "helpers/libraryItem";
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
import { useLibraryItemUserStatus } from "hooks/useLibraryItemUserStatus";
import { Paginator } from "components/Containers/Paginator";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -72,16 +71,16 @@ interface Props extends AppLayoutRequired {}
const Library = (props: Props): JSX.Element => {
const hoverable = useDeviceSupportsHover();
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const { libraryItemUserStatus } = useLibraryItemUserStatus();
const sortingMethods = useMemo(
() => [
{ meiliAttribute: "sortable_name:asc", displayedName: langui.name },
{ meiliAttribute: "sortable_date:asc", displayedName: langui.release_date },
{ meiliAttribute: "sortable_price:asc", displayedName: langui.price },
{ meiliAttribute: "sortable_name:asc", displayedName: format("name") },
{ meiliAttribute: "sortable_date:asc", displayedName: format("release_date") },
{ meiliAttribute: "sortable_price:asc", displayedName: format("price") },
],
[langui.name, langui.price, langui.release_date]
[format]
);
const router = useTypedRouter(queryParamSchema);
@ -249,15 +248,15 @@ const Library = (props: Props): JSX.Element => {
<SubPanel>
<PanelHeader
icon="auto_stories"
title={langui.library}
description={langui.library_description}
title={format("library")}
description={format("library_description")}
/>
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."}
placeholder={format("search_title")}
value={query}
onChange={(name) => {
setPage(1);
@ -270,10 +269,10 @@ const Library = (props: Props): JSX.Element => {
}}
/>
<WithLabel label={langui.order_by}>
<WithLabel label={format("order_by")}>
<Select
className="w-full"
options={sortingMethods.map((item) => item.displayedName ?? "")}
options={sortingMethods.map((item) => item.displayedName)}
value={sortingMethod}
onChange={(newSort) => {
setPage(1);
@ -286,7 +285,7 @@ const Library = (props: Props): JSX.Element => {
/>
</WithLabel>
<WithLabel label={langui.show_subitems}>
<WithLabel label={format("show_subitems")}>
<Switch
value={showSubitems}
onClick={() => {
@ -297,7 +296,7 @@ const Library = (props: Props): JSX.Element => {
/>
</WithLabel>
<WithLabel label={langui.show_primary_items}>
<WithLabel label={format("show_primary_items")}>
<Switch
value={showPrimaryItems}
onClick={() => {
@ -308,7 +307,7 @@ const Library = (props: Props): JSX.Element => {
/>
</WithLabel>
<WithLabel label={langui.show_secondary_items}>
<WithLabel label={format("show_secondary_items")}>
<Switch
value={showSecondaryItems}
onClick={() => {
@ -320,7 +319,7 @@ const Library = (props: Props): JSX.Element => {
</WithLabel>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<WithLabel label={format("always_show_info")}>
<Switch
value={keepInfoVisible}
onClick={() => {
@ -335,7 +334,7 @@ const Library = (props: Props): JSX.Element => {
className="mt-4"
buttonsProps={[
{
tooltip: langui.only_display_items_i_want,
tooltip: format("only_display_items_i_want"),
icon: "favorite",
onClick: () => {
setPage(1);
@ -345,7 +344,7 @@ const Library = (props: Props): JSX.Element => {
active: filterUserStatus === LibraryItemUserStatus.Want,
},
{
tooltip: langui.only_display_items_i_have,
tooltip: format("only_display_items_i_have"),
icon: "back_hand",
onClick: () => {
setPage(1);
@ -355,7 +354,7 @@ const Library = (props: Props): JSX.Element => {
active: filterUserStatus === LibraryItemUserStatus.Have,
},
{
tooltip: langui.only_display_unmarked_items,
tooltip: format("only_display_unmarked_items"),
icon: "nearby_off",
onClick: () => {
setPage(1);
@ -365,8 +364,8 @@ const Library = (props: Props): JSX.Element => {
active: filterUserStatus === LibraryItemUserStatus.None,
},
{
tooltip: langui.only_display_unmarked_items,
text: langui.all,
tooltip: format("only_display_unmarked_items"),
text: format("all"),
onClick: () => {
setPage(1);
setFilterUserStatus(undefined);
@ -379,7 +378,7 @@ const Library = (props: Props): JSX.Element => {
<Button
className="mt-8"
text={langui.reset_all_filters}
text={format("reset_all_filters")}
icon="settings_backup_restore"
onClick={() => {
setQuery(DEFAULT_FILTERS_STATE.query);
@ -455,9 +454,9 @@ export default Library;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.library ?? "Library"),
openGraph: getOpenGraph(format, format("library")),
};
return {
props: props,

View File

@ -1,44 +0,0 @@
import { GetStaticProps } from "next";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Containers/SubPanel";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {}
const Merch = (props: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
return (
<AppLayout
subPanel={
<SubPanel>
<PanelHeader icon="store" title={langui.merch} description={langui.merch_description} />
</SubPanel>
}
{...props}
/>
);
};
export default Merch;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.merch ?? "Merch"),
};
return {
props: props,
};
};

View File

@ -11,6 +11,7 @@ import { prettyTerminalBoxedTitle } from "helpers/terminal";
import { prettyMarkdown } from "helpers/description";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
/*
*
* PAGE
@ -19,7 +20,7 @@ import { useAtomGetter } from "helpers/atoms";
interface Props extends PostStaticProps {}
const LibrarySlug = (props: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const router = useRouter();
@ -36,7 +37,7 @@ const LibrarySlug = (props: Props): JSX.Element => {
return (
<PostPage
returnHref="/news"
returnTitle={langui.news}
returnTitle={format("news")}
displayCredits
displayThumbnailHeader
displayToc

View File

@ -20,7 +20,6 @@ import {
import { getOpenGraph } from "helpers/openGraph";
import { TranslatedPreviewCard } from "components/PreviewCard";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { Terminal } from "components/Cli/Terminal";
import { atoms } from "contexts/atoms";
@ -30,6 +29,8 @@ import { MeiliIndices, MeiliPost } from "shared/meilisearch-graphql-typings/meil
import { useTypedRouter } from "hooks/useTypedRouter";
import { prettySlug } from "helpers/formatters";
import { Paginator } from "components/Containers/Paginator";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -55,7 +56,7 @@ const queryParamSchema = z.object({
interface Props extends AppLayoutRequired {}
const News = ({ ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const hoverable = useDeviceSupportsHover();
const router = useTypedRouter(queryParamSchema);
@ -115,13 +116,17 @@ const News = ({ ...otherProps }: Props): JSX.Element => {
const subPanel = (
<SubPanel>
<PanelHeader icon="newspaper" title={langui.news} description={langui.news_description} />
<PanelHeader
icon="newspaper"
title={format("news")}
description={format("news_description")}
/>
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."}
placeholder={format("search_title")}
value={query}
onChange={(name) => {
setQuery(name);
@ -134,7 +139,7 @@ const News = ({ ...otherProps }: Props): JSX.Element => {
/>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<WithLabel label={format("always_show_info")}>
<Switch
value={keepInfoVisible}
onClick={() => {
@ -147,7 +152,7 @@ const News = ({ ...otherProps }: Props): JSX.Element => {
<Button
className="mt-8"
text={langui.reset_all_filters}
text={format("reset_all_filters")}
icon="settings_backup_restore"
onClick={() => {
setQuery(DEFAULT_FILTERS_STATE.query);
@ -220,9 +225,9 @@ export default News;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.news ?? "News"),
openGraph: getOpenGraph(format, format("news")),
};
return {
props: props,

View File

@ -19,11 +19,12 @@ import { getOpenGraph } from "helpers/openGraph";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
import { getDescription } from "helpers/description";
import { cIf, cJoin } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { Terminal } from "components/Cli/Terminal";
import { prettyTerminalBoxedTitle, prettyTerminalUnderlinedTitle } from "helpers/terminal";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -35,7 +36,7 @@ interface Props extends AppLayoutRequired {
}
const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const router = useRouter();
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const { showLightBox } = useAtomGetter(atoms.lightBox);
@ -51,7 +52,7 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
const subPanel = (
<SubPanel>
<ReturnButton href={`/wiki`} title={langui.wiki} displayOnlyOn={"3ColumnsLayout"} />
<ReturnButton href={`/wiki`} title={format("wiki")} displayOnlyOn={"3ColumnsLayout"} />
</SubPanel>
);
@ -59,7 +60,7 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
<ContentPanel width={ContentPanelWidthSizes.Large}>
<ReturnButton
href={`/wiki`}
title={langui.wiki}
title={format("wiki")}
displayOnlyOn={"1ColumnLayout"}
className="mb-10"
/>
@ -98,7 +99,9 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
<div className="my-4 grid gap-4 p-4">
{page.categories?.data && page.categories.data.length > 0 && (
<>
<p className="font-headers text-xl font-bold">{langui.categories}</p>
<p className="font-headers text-xl font-bold">
{format("category", { count: page.categories.data.length })}
</p>
<div className="flex flex-row flex-wrap place-content-center gap-2">
{filterHasAttributes(page.categories.data, ["attributes"] as const).map(
@ -112,7 +115,7 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
{page.tags?.data && page.tags.data.length > 0 && (
<>
<p className="font-headers text-xl font-bold">{langui.tags}</p>
<p className="font-headers text-xl font-bold">{format("tags")}</p>
<div className="flex flex-row flex-wrap place-content-center gap-2">
{filterHasAttributes(page.tags.data, ["attributes"] as const).map((tag) => (
<Chip
@ -130,7 +133,7 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
{isDefinedAndNotEmpty(selectedTranslation.summary) && (
<div className="mb-12">
<p className="font-headers text-lg font-bold">{langui.summary}</p>
<p className="font-headers text-lg font-bold">{format("summary")}</p>
<p>{selectedTranslation.summary}</p>
</div>
)}
@ -188,13 +191,13 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
}`
)}${
isDefinedAndNotEmpty(selectedTranslation?.summary)
? `${prettyTerminalUnderlinedTitle(langui.summary)}${selectedTranslation?.summary}`
? `${prettyTerminalUnderlinedTitle(format("summary"))}${selectedTranslation?.summary}`
: ""
}${
page.definitions && page.definitions.length > 0
? `${filterHasAttributes(page.definitions, ["translations"] as const).map(
(definition, index) =>
`${prettyTerminalUnderlinedTitle(`${langui.definition} ${index + 1}`)}${
`${prettyTerminalUnderlinedTitle(format("definition_x", { x: index + 1 }))}${
staticSmartLanguage({
items: filterHasAttributes(definition.translations, [
"language.data.attributes.code",
@ -228,7 +231,7 @@ export default WikiPage;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const slug =
context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "";
const page = await sdk.getWikiPage({
@ -239,12 +242,12 @@ export const getStaticProps: GetStaticProps = async (context) => {
const { title, description } = (() => {
const chipsGroups = {
[langui.tags ?? "Tags"]: filterHasAttributes(page.wikiPages.data[0].attributes.tags?.data, [
[format("tags")]: filterHasAttributes(page.wikiPages.data[0].attributes.tags?.data, [
"attributes",
] as const).map(
(tag) => tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
),
[langui.categories ?? "Categories"]: filterHasAttributes(
[format("category", { count: Infinity })]: filterHasAttributes(
page.wikiPages.data[0].attributes.categories?.data,
["attributes"] as const
).map((category) => category.attributes.short),
@ -274,7 +277,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
const props: Props = {
page: page.wikiPages.data[0].attributes as WikiPageWithTranslations,
openGraph: getOpenGraph(langui, title, description, thumbnail),
openGraph: getOpenGraph(format, title, description, thumbnail),
};
return {
props: props,

View File

@ -13,7 +13,6 @@ import {
} from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { prettySlug } from "helpers/formatters";
import { getStatusDescription } from "helpers/others";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph";
import { useSmartLanguage } from "hooks/useSmartLanguage";
@ -26,9 +25,8 @@ import { TranslatedProps } from "types/TranslatedProps";
import { TranslatedNavOption } from "components/PanelComponents/NavOption";
import { useIntersectionList } from "hooks/useIntersectionList";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -41,7 +39,7 @@ interface Props extends AppLayoutRequired {
}
const Chronology = ({ chronologyItems, chronologyEras, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const ids = filterHasAttributes(chronologyEras, ["attributes"] as const).map(
(era) => era.attributes.slug
);
@ -50,7 +48,7 @@ const Chronology = ({ chronologyItems, chronologyEras, ...otherProps }: Props):
const subPanel = (
<SubPanel>
<ReturnButton href="/wiki" title={langui.wiki} displayOnlyOn="3ColumnsLayout" />
<ReturnButton href="/wiki" title={format("wiki")} displayOnlyOn="3ColumnsLayout" />
<HorizontalLine />
@ -81,7 +79,7 @@ const Chronology = ({ chronologyItems, chronologyEras, ...otherProps }: Props):
<ContentPanel>
<ReturnButton
href="/wiki"
title={langui.wiki}
title={format("wiki")}
displayOnlyOn="1ColumnLayout"
className="mb-10"
/>
@ -119,7 +117,7 @@ export default Chronology;
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const chronologyItems = await sdk.getChronologyItems();
const chronologyEras = await sdk.getEras();
if (!chronologyItems.chronologyItems || !chronologyEras.chronologyEras) return { notFound: true };
@ -127,7 +125,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
const props: Props = {
chronologyItems: chronologyItems.chronologyItems.data,
chronologyEras: chronologyEras.chronologyEras.data,
openGraph: getOpenGraph(langui, langui.chronology ?? "Chronology"),
openGraph: getOpenGraph(format, format("chronology")),
};
return {
props: props,
@ -302,7 +300,7 @@ interface ChronologyEventProps {
}
export const ChronologyEvent = ({ event, id }: ChronologyEventProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { format, formatStatusDescription } = useFormat();
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: event.translations ?? [],
languageExtractor: useCallback(
@ -322,7 +320,7 @@ export const ChronologyEvent = ({ event, id }: ChronologyEventProps): JSX.Elemen
{selectedTranslation.status !==
Enum_Componenttranslationschronologyitem_Status.Done && (
<ToolTip
content={getStatusDescription(selectedTranslation.status, langui)}
content={formatStatusDescription(selectedTranslation.status)}
maxWidth={"20rem"}>
<Chip text={selectedTranslation.status} />
</ToolTip>
@ -334,7 +332,7 @@ export const ChronologyEvent = ({ event, id }: ChronologyEventProps): JSX.Elemen
) : (
<div className="flex items-center gap-1">
<Ico icon="warning" className="!text-sm" />
{langui.no_source_warning}
{format("no_source_warning")}
</div>
)}
</p>
@ -354,7 +352,7 @@ export const ChronologyEvent = ({ event, id }: ChronologyEventProps): JSX.Elemen
<p className="whitespace-pre-line">{selectedTranslation.description}</p>
)}
{selectedTranslation.note && <em>{`${langui.notes}: ${selectedTranslation.note}`}</em>}
{selectedTranslation.note && <em>{`${format("notes")}: ${selectedTranslation.note}`}</em>}
</>
)}
</div>

View File

@ -17,14 +17,13 @@ import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/as
import { prettySlug } from "helpers/formatters";
import { getOpenGraph } from "helpers/openGraph";
import { TranslatedPreviewCard } from "components/PreviewCard";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useTypedRouter } from "hooks/useTypedRouter";
import { MeiliIndices, MeiliWikiPage } from "shared/meilisearch-graphql-typings/meiliTypes";
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search";
import { Paginator } from "components/Containers/Paginator";
import { useFormat } from "hooks/useFormat";
import { getFormat } from "helpers/i18n";
/*
*
@ -51,7 +50,7 @@ interface Props extends AppLayoutRequired {}
const Wiki = (props: Props): JSX.Element => {
const hoverable = useDeviceSupportsHover();
const langui = useAtomGetter(atoms.localData.langui);
const { format } = useFormat();
const router = useTypedRouter(queryParamSchema);
const [query, setQuery] = useState(router.query.query ?? DEFAULT_FILTERS_STATE.query);
@ -103,15 +102,15 @@ const Wiki = (props: Props): JSX.Element => {
<SubPanel>
<PanelHeader
icon="travel_explore"
title={langui.wiki}
description={langui.wiki_description}
title={format("wiki")}
description={format("wiki_description")}
/>
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."}
placeholder={format("search_title")}
value={query}
onChange={(name) => {
setPage(1);
@ -125,7 +124,7 @@ const Wiki = (props: Props): JSX.Element => {
/>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<WithLabel label={format("always_show_info")}>
<Switch
value={keepInfoVisible}
onClick={() => {
@ -138,7 +137,7 @@ const Wiki = (props: Props): JSX.Element => {
<Button
className="mt-8"
text={langui.reset_all_filters}
text={format("reset_all_filters")}
icon="settings_backup_restore"
onClick={() => {
setPage(1);
@ -150,9 +149,9 @@ const Wiki = (props: Props): JSX.Element => {
<HorizontalLine />
<p className="mb-4 font-headers text-xl font-bold">{langui.special_pages}</p>
<p className="mb-4 font-headers text-xl font-bold">{format("special_pages")}</p>
<NavOption title={langui.chronology} url="/wiki/chronology" border />
<NavOption title={format("chronology")} url="/wiki/chronology" border />
</SubPanel>
);
@ -212,9 +211,9 @@ export default Wiki;
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const { format } = getFormat(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.wiki ?? "Wiki"),
openGraph: getOpenGraph(format, format("wiki")),
};
return {
props: props,