diff --git a/.eslintignore b/.eslintignore index 54a9d41..4dd37a6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ src/graphql/generated.ts +src/graphql/icuParams.ts src/shared .eslintrc.js graphql-codegen.config.js diff --git a/README.md b/README.md index 88bb83b..07bb428 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,25 @@ # Accords-library.com + [![Node.js CI](https://github.com/Accords-Library/accords-library.com/actions/workflows/node.js.yml/badge.svg?branch=main)](https://github.com/Accords-Library/accords-library.com/actions/workflows/node.js.yml) [![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 + +Accord’s Library is a fan-site that aims at gathering and archiving all of Yoko Taro’s 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) +#### [Content Management System](https://github.com/Accords-Library/strapi.accords-library.com) -- CMS: Stapi +- CMS: [Stapi](https://strapi.io/) - GraphQL endpoint - Multilanguage support - Markdown format for the rich text fields - - Use webhooks to notify the front-end and image processor of updates + - Use webhooks to notify the front-end, search engine, and image processor of updates #### [Image Processor](https://github.com/Accords-Library/img.accords-library.com) @@ -22,51 +29,81 @@ - Large: 2048x2048, quality 80, .webp - Og: 512x512, quality 60, .jpg +#### [Search Engine](https://github.com/Accords-Library/search.accords-library.com) + +- Search Engine: [Meilisearch](https://www.meilisearch.com/) + #### [Front](https://github.com/Accords-Library/accords-library.com) (this repository) - Language: [TypeScript](https://www.typescriptlang.org/) + - Framework: [Next.js 13](https://nextjs.org/) (React 18) + - 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! + +- 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 + - 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 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 + - 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 + +- 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 the working isn't available in one language + - 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. + - 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) @@ -76,6 +113,12 @@ - Other - Custom book reader based on [Okuma-Reader](https://github.com/DrMint/Okuma-Reader) - 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 special "Terminal" mode. Can you find it? + +## Design + +Check out our [Design System Showcase](https://accords-library.com/dev/showcase/design-system)! ## Installation diff --git a/docs/project-mind-map.minder b/docs/project-mind-map.minder new file mode 100644 index 0000000..47ae629 --- /dev/null +++ b/docs/project-mind-map.minder @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GraphQL queries + + + + + Webhook + + + + + GraphQL mutations + + + + + Sends events + + + + + Provides the images + + + + + Provides search results + + + + + GraphQL queries + + + + + Links to + + + + + Python script + + + + + Provides the videos + + + + + Webhook + + + + + diff --git a/docs/project-mind-map.png b/docs/project-mind-map.png new file mode 100644 index 0000000..24e5f2e Binary files /dev/null and b/docs/project-mind-map.png differ diff --git a/package-lock.json b/package-lock.json index ca530aa..470c4aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1f652f9..c2865d2 100644 --- a/package.json +++ b/package.json @@ -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 prettier && npm run unused-exports && 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", diff --git a/public/local-data/websiteInterfaces.json b/public/local-data/websiteInterfaces.json index 73ae013..e0dfce5 100644 --- a/public/local-data/websiteInterfaces.json +++ b/public/local-data/websiteInterfaces.json @@ -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 website’s content is made available under [CC-BY-SA](https://creativecommons.org/licenses/by-sa/4.0/) unless otherwise noted.", "copyright_notice": "Accord’s 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 Yokoverse’s 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! We’re 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 } } ] diff --git a/src/components/AnchorShare.tsx b/src/components/AnchorShare.tsx index 15fde50..a44db33 100644 --- a/src/components/AnchorShare.tsx +++ b/src/components/AnchorShare.tsx @@ -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 ( - - + + + )} diff --git a/src/components/Containers/Paginator.tsx b/src/components/Containers/Paginator.tsx index 56e0e4b..547036a 100644 --- a/src/components/Containers/Paginator.tsx +++ b/src/components/Containers/Paginator.tsx @@ -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 ( {is3ColumnsLayout && } - {langui.no_results_message} + {format("no_results_message")} {!is3ColumnsLayout && } diff --git a/src/components/Library/PreviewCardCTAs.tsx b/src/components/Library/PreviewCardCTAs.tsx index 887170c..8abe734 100644 --- a/src/components/Library/PreviewCardCTAs.tsx +++ b/src/components/Library/PreviewCardCTAs.tsx @@ -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 ( { "flex flex-row flex-wrap place-content-center place-items-center", cIf(expand, "gap-4", "gap-2") )}> - + { event.preventDefault(); @@ -46,10 +45,10 @@ export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => { }} /> - + { event.preventDefault(); diff --git a/src/components/Markdown/Markdawn.tsx b/src/components/Markdown/Markdawn.tsx index 03cccad..ca9ddea 100644 --- a/src/components/Markdown/Markdawn.tsx +++ b/src/components/Markdown/Markdawn.tsx @@ -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 && } - {langui.table_of_contents} + {format("table_of_contents")} { - 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)) && ( - + )} > diff --git a/src/components/Panels/MainPanel.tsx b/src/components/Panels/MainPanel.tsx index d780146..6d15879 100644 --- a/src/components/Panels/MainPanel.tsx +++ b/src/components/Panels/MainPanel.tsx @@ -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") )}> {langui.open_settings}} + content={{format("open_settings")}} placement={isMainPanelReduced ? "right" : "top"}> { @@ -83,7 +84,7 @@ export const MainPanel = (): JSX.Element => { /> {langui.open_search}} + content={{format("open_search")}} placement={isMainPanelReduced ? "right" : "top"}> { @@ -102,32 +103,32 @@ export const MainPanel = (): JSX.Element => { @@ -136,7 +137,7 @@ export const MainPanel = (): JSX.Element => { @@ -144,7 +145,7 @@ export const MainPanel = (): JSX.Element => { */} @@ -152,30 +153,30 @@ export const MainPanel = (): JSX.Element => { {(!isMainPanelReduced || !is3ColumnsLayout) && } - {isDefinedAndNotEmpty(langui.licensing_notice) && ( + {isDefinedAndNotEmpty(format("licensing_notice")) && ( - + )} @@ -199,9 +200,9 @@ export const MainPanel = (): JSX.Element => { /> - {isDefinedAndNotEmpty(langui.copyright_notice) && ( + {isDefinedAndNotEmpty(format("copyright_notice")) && ( - + )} diff --git a/src/components/Panels/SearchPopup.tsx b/src/components/Panels/SearchPopup.tsx index 1d52ab5..de98404 100644 --- a/src/components/Panels/SearchPopup.tsx +++ b/src/components/Panels/SearchPopup.tsx @@ -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>(); const [contents, setContents] = useState>(); const [videos, setVideos] = useState>(); @@ -178,14 +179,14 @@ export const SearchPopup = (): JSX.Element => { fillViewport> - {langui.search} + {format("search")} - + {isDefined(libraryItems) && ( { {isDefined(contents) && ( @@ -276,7 +277,7 @@ export const SearchPopup = (): JSX.Element => { {isDefined(wikiPages) && ( @@ -327,7 +328,7 @@ export const SearchPopup = (): JSX.Element => { {isDefined(posts) && ( @@ -369,7 +370,7 @@ export const SearchPopup = (): JSX.Element => { {isDefined(videos) && ( @@ -424,26 +425,30 @@ const SearchResultSection = ({ href, totalHits, children, -}: SearchResultSectionProps) => ( - <> - {isDefined(totalHits) && totalHits > 0 && ( - - - - - - {title} - {isDefined(totalHits) && totalHits > SEARCH_LIMIT && ( - /* TODO: Langui */ - {`(showing ${SEARCH_LIMIT} out of ${totalHits} results)`} - )} - - +}: SearchResultSectionProps) => { + const { format } = useFormat(); + return ( + <> + {isDefined(totalHits) && totalHits > 0 && ( + + + + + + {title} + {isDefined(totalHits) && totalHits > SEARCH_LIMIT && ( + + ({format("showing_x_out_of_y_results", { x: SEARCH_LIMIT, y: totalHits })}) + + )} + + + + {children} - {children} - - )} - > -); + )} + > + ); +}; diff --git a/src/components/Panels/SettingsPopup.tsx b/src/components/Panels/SettingsPopup.tsx index 6647749..b9fd163 100644 --- a/src/components/Panels/SettingsPopup.tsx +++ b/src/components/Panels/SettingsPopup.tsx @@ -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 => { }}> - {langui.settings} + {format("settings")} { )}> {router.locales && ( - {langui.languages} + {format("language", { count: preferredLanguages.length })} {preferredLanguages.length > 0 && ( ({ @@ -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") )}> - {langui.theme} + {format("theme")} { 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"), }, ]} /> - {langui.currency} + {format("currency")} { - {langui.font_size} + {format("font_size")} { - {langui.font} + {format("font")} { - {langui.player_name} + {format("player_name")} { - 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 && ( - {langui.status}: + {format("status")}: diff --git a/src/components/RecorderChip.tsx b/src/components/RecorderChip.tsx index ec4a452..17fa2a6 100644 --- a/src/components/RecorderChip.tsx +++ b/src/components/RecorderChip.tsx @@ -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 ( { {recorder.username} {recorder.languages?.data && recorder.languages.data.length > 0 && ( - {langui.languages}: + {format("language", { count: recorder.languages.data.length })}: {filterHasAttributes(recorder.languages.data, ["attributes"] as const).map( (language) => ( @@ -52,7 +51,7 @@ export const RecorderChip = ({ recorder }: Props): JSX.Element => { )} {recorder.pronouns && ( - {langui.pronouns}: + {format("pronouns")}: )} diff --git a/src/components/SmartList.tsx b/src/components/SmartList.tsx index 5913f9f..279a184 100644 --- a/src/components/SmartList.tsx +++ b/src/components/SmartList.tsx @@ -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 { name: string; @@ -71,7 +72,7 @@ export const SmartList = ({ className, }: Props): 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 = ({ className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl first-of-type:pt-0"> {group.name} - + )} ({ */ const DefaultRenderWhenEmpty = () => { + const { format } = useFormat(); const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout); - const langui = useAtomGetter(atoms.localData.langui); return ( {is3ColumnsLayout && } - {langui.no_results_message} + {format("no_results_message")} {!is3ColumnsLayout && } diff --git a/src/components/ThumbnailHeader.tsx b/src/components/ThumbnailHeader.tsx index 7bbed9e..bcd2d06 100644 --- a/src/components/ThumbnailHeader.tsx +++ b/src/components/ThumbnailHeader.tsx @@ -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 = ({ {type?.data?.attributes && ( - {langui.type} + {format("type", { count: 1 })} 0 && ( - {langui.categories} + {format("category", { count: categories.data.length })} {filterHasAttributes(categories.data, ["attributes", "id"] as const).map( (category) => ( diff --git a/src/components/Wiki/DefinitionCard.tsx b/src/components/Wiki/DefinitionCard.tsx index d06c648..b5ce650 100644 --- a/src/components/Wiki/DefinitionCard.tsx +++ b/src/components/Wiki/DefinitionCard.tsx @@ -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 ( <> - {`${langui.definition} ${index}`} + {format("definition_x", { x: index })} {translations.length > 1 && ( <> @@ -53,7 +53,7 @@ const DefinitionCard = ({ source, translations = [], index, categories }: Props) <> @@ -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") )}> - {langui.source}: + {format("source")}: )} diff --git a/src/contexts/localData.ts b/src/contexts/localData.ts index be54704..24ca9e0 100644 --- a/src/contexts/localData.ts +++ b/src/contexts/localData.ts @@ -21,11 +21,13 @@ import { LocalDataFile } from "graphql/fetchLocalData"; const languages = atomPairing(atom([])); const currencies = atomPairing(atom([])); const langui = atomPairing(atom({})); +const fallbackLangui = atomPairing(atom({})); 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(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]); }; diff --git a/src/graphql/getPostStaticProps.ts b/src/graphql/getPostStaticProps.ts index 26b6118..30f89e7 100644 --- a/src/graphql/getPostStaticProps.ts +++ b/src/graphql/getPostStaticProps.ts @@ -41,7 +41,7 @@ export const getPostStaticProps = [langui.release_date ?? "Release date"]: [ prettyDate(post.posts.data[0].attributes.date, context.locale), ], - [langui.categories ?? "Categories"]: filterHasAttributes( + [langui.category ?? "Categories"]: filterHasAttributes( post.posts.data[0].attributes.categories?.data, ["attributes"] as const ).map((category) => category.attributes.short), diff --git a/src/graphql/icuParams.ts b/src/graphql/icuParams.ts new file mode 100644 index 0000000..67ceb95 --- /dev/null +++ b/src/graphql/icuParams.ts @@ -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 }; +} diff --git a/src/graphql/icuToTypescript.ts b/src/graphql/icuToTypescript.ts new file mode 100644 index 0000000..6ed8144 --- /dev/null +++ b/src/graphql/icuToTypescript.ts @@ -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(); +} diff --git a/src/graphql/operations/local-data/localDataGetWebsiteInterfaces.graphql b/src/graphql/operations/local-data/localDataGetWebsiteInterfaces.graphql index 77f66cd..c78d3eb 100644 --- a/src/graphql/operations/local-data/localDataGetWebsiteInterfaces.graphql +++ b/src/graphql/operations/local-data/localDataGetWebsiteInterfaces.graphql @@ -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 } } } diff --git a/src/helpers/formatters.ts b/src/helpers/formatters.ts index 123139f..6af4438 100644 --- a/src/helpers/formatters.ts +++ b/src/helpers/formatters.ts @@ -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: diff --git a/src/helpers/others.ts b/src/helpers/others.ts index 0d3c4a8..a7195bd 100644 --- a/src/helpers/others.ts +++ b/src/helpers/others.ts @@ -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 = ( map: Map, callbackfn: (key: K, value: V, index: number) => U, diff --git a/src/hooks/useFormat.ts b/src/hooks/useFormat.ts new file mode 100644 index 0000000..1285b8f --- /dev/null +++ b/src/hooks/useFormat.ts @@ -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; + +export type ContentStatus = "Done" | "Draft" | "Incomplete" | "Review"; + +const componentMetadataToWording: Record = { + 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: ( + 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 => { + 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, + }; +}; diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 3112b18..f6cac38 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -5,8 +5,7 @@ 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"; /* * ╭────────╮ @@ -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 ( { className="animate-zoom-in drop-shadow-lg shadow-shade" /> - {langui.page_not_found} + {format("page_not_found")} diff --git a/src/pages/500.tsx b/src/pages/500.tsx index c4605be..1c81153 100644 --- a/src/pages/500.tsx +++ b/src/pages/500.tsx @@ -5,8 +5,7 @@ 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"; /* * ╭────────╮ @@ -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 ( { className="animate-zoom-in drop-shadow-lg shadow-shade" /> - {langui.page_not_found} + {format("page_not_found")} diff --git a/src/pages/about-us/accords-handbook.tsx b/src/pages/about-us/accords-handbook.tsx index 4cb9d13..368e57c 100644 --- a/src/pages/about-us/accords-handbook.tsx +++ b/src/pages/about-us/accords-handbook.tsx @@ -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 ( diff --git a/src/pages/about-us/contact.tsx b/src/pages/about-us/contact.tsx index 2c1ddee..45c3b20 100644 --- a/src/pages/about-us/contact.tsx +++ b/src/pages/about-us/contact.tsx @@ -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 = ""; }}> - {langui.name}: + {format("name")}: { - {langui.email}: + {format("email")}: { required disabled={formState !== "stale"} /> - {langui.email_gdpr_notice} + {format("email_gdpr_notice")} - {langui.message}: + {format("message")}: { @@ -168,7 +169,7 @@ const AboutUs = (props: PostStaticProps): JSX.Element => { { - const langui = useAtomGetter(atoms.localData.langui); + const { format } = useFormat(); return ( - - - - + + + + } {...props} diff --git a/src/pages/about-us/legality.tsx b/src/pages/about-us/legality.tsx index a753c4d..2a45dc8 100644 --- a/src/pages/about-us/legality.tsx +++ b/src/pages/about-us/legality.tsx @@ -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 ( diff --git a/src/pages/about-us/sharing-policy.tsx b/src/pages/about-us/sharing-policy.tsx index 850523e..c6f4b55 100644 --- a/src/pages/about-us/sharing-policy.tsx +++ b/src/pages/about-us/sharing-policy.tsx @@ -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 ( diff --git a/src/pages/archives/index.tsx b/src/pages/archives/index.tsx index 423d447..256b018 100644 --- a/src/pages/archives/index.tsx +++ b/src/pages/archives/index.tsx @@ -6,8 +6,7 @@ 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"; /* * ╭────────╮ @@ -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 = ( - + diff --git a/src/pages/archives/videos/c/[uid].tsx b/src/pages/archives/videos/c/[uid].tsx index 75e38b7..5a43b90 100644 --- a/src/pages/archives/videos/c/[uid].tsx +++ b/src/pages/archives/videos/c/[uid].tsx @@ -14,8 +14,6 @@ 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 +26,7 @@ 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"; /* * ╭─────────────╮ @@ -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 => { @@ -167,14 +159,16 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => { { setPage(1); @@ -187,10 +181,10 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => { }} /> - + 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 => { /> - + { @@ -213,14 +207,14 @@ const Channel = ({ channel, ...otherProps }: Props): JSX.Element => { {hoverable && ( - + )} { setOnlyShowGone(DEFAULT_FILTERS_STATE.onlyShowGone); diff --git a/src/pages/archives/videos/index.tsx b/src/pages/archives/videos/index.tsx index 1a74736..0a6961c 100644 --- a/src/pages/archives/videos/index.tsx +++ b/src/pages/archives/videos/index.tsx @@ -14,8 +14,6 @@ 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 +24,7 @@ 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"; /* * ╭─────────────╮ @@ -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" /> - + { setPage(1); @@ -177,10 +173,10 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => { }} /> - + item.displayedName ?? "")} + options={sortingMethods.map((item) => item.displayedName)} value={sortingMethod} onChange={(newSort) => { setPage(1); @@ -193,7 +189,7 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => { /> - + { @@ -203,14 +199,14 @@ const Videos = ({ ...otherProps }: Props): JSX.Element => { {hoverable && ( - + )} { setPage(1); diff --git a/src/pages/archives/videos/v/[uid].tsx b/src/pages/archives/videos/v/[uid].tsx index e62c775..77557b4 100644 --- a/src/pages/archives/videos/v/[uid].tsx +++ b/src/pages/archives/videos/v/[uid].tsx @@ -19,6 +19,7 @@ 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"; /* * ╭────────╮ @@ -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 = ( - - - + + + ); @@ -54,7 +55,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => { @@ -97,7 +98,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => { )} - + @@ -106,7 +107,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => { {video.channel?.data?.attributes && ( - {langui.channel} + {format("channel")} { /> {`${video.channel.data.attributes.subscribers.toLocaleString()} - ${langui.subscribers?.toLowerCase()}`} + ${format("subscribers").toLowerCase()}`} @@ -124,7 +125,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => { - {langui.description} + {format("description")} {video.description} diff --git a/src/pages/chronicles/[slug]/index.tsx b/src/pages/chronicles/[slug]/index.tsx index 75f36f0..adaffca 100644 --- a/src/pages/chronicles/[slug]/index.tsx +++ b/src/pages/chronicles/[slug]/index.tsx @@ -18,10 +18,9 @@ import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/local 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"; /* * ╭────────╮ @@ -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 = @@ -119,7 +118,11 @@ const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element = const subPanel = ( - + @@ -194,7 +197,7 @@ export const getStaticProps: GetStaticProps = async (context) => { chronicle.chronicles.data[0].attributes.contents.data[0].attributes.type?.data ?.attributes?.titles?.[0]?.title, ], - [langui.categories ?? "Categories"]: filterHasAttributes( + [langui.category ?? "Categories"]: filterHasAttributes( chronicle.chronicles.data[0].attributes.contents.data[0].attributes.categories ?.data, ["attributes"] as const diff --git a/src/pages/chronicles/index.tsx b/src/pages/chronicles/index.tsx index b218662..6517e54 100644 --- a/src/pages/chronicles/index.tsx +++ b/src/pages/chronicles/index.tsx @@ -10,8 +10,7 @@ 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"; /* * ╭────────╮ @@ -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 = ( diff --git a/src/pages/contents/[slug].tsx b/src/pages/contents/[slug].tsx index 317cc07..a26ec11 100644 --- a/src/pages/contents/[slug].tsx +++ b/src/pages/contents/[slug].tsx @@ -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"; @@ -35,6 +34,7 @@ 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"; /* * ╭────────╮ @@ -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 => { {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")} {selectedTranslation.text_set.source_language.data.attributes.code !== selectedTranslation.language?.data?.attributes?.code && ( - {langui.source_language}: + {format("source_language")}: { )} - {langui.status}: + {format("status")}: @@ -132,7 +131,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => { {selectedTranslation.text_set.transcribers && selectedTranslation.text_set.transcribers.data.length > 0 && ( - {langui.transcribers}: + {format("transcribers")}: {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 && ( - {langui.translators}: + {format("translators")}: {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 && ( - {langui.proofreaders}: + {format("proofreaders")}: {filterHasAttributes(selectedTranslation.text_set.proofreaders.data, [ "attributes", @@ -182,7 +181,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => { {isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && ( - {langui.notes}: + {format("notes")}: @@ -210,7 +209,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => { <> - {langui.source} + {format("source")} {filterHasAttributes(content.ranged_contents.data, [ "attributes.library_item.data.attributes", @@ -283,7 +282,7 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => { {previousContent?.attributes && ( - {langui.previous_content} + {format("previous_content")} { {nextContent?.attributes && ( <> - {langui.followup_content} + {format("followup_content")} { [langui.type ?? "Type"]: [ content.contents.data[0].attributes.type?.data?.attributes?.titles?.[0]?.title, ], - [langui.categories ?? "Categories"]: filterHasAttributes( + [langui.category ?? "Categories"]: filterHasAttributes( content.contents.data[0].attributes.categories?.data, ["attributes"] as const ).map((category) => category.attributes.short), diff --git a/src/pages/contents/all.tsx b/src/pages/contents/all.tsx index 4cd0234..8bf8e53 100644 --- a/src/pages/contents/all.tsx +++ b/src/pages/contents/all.tsx @@ -22,14 +22,13 @@ 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"; /* * ╭─────────────╮ @@ -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( @@ -131,19 +130,19 @@ const Contents = (props: Props): JSX.Element => { - + { setPage(1); @@ -156,10 +155,10 @@ const Contents = (props: Props): JSX.Element => { }} /> - + item.displayedName ?? "")} + options={sortingMethods.map((item) => item.displayedName)} value={sortingMethod} onChange={(newSort) => { setPage(1); @@ -173,7 +172,7 @@ const Contents = (props: Props): JSX.Element => { {hoverable && ( - + { @@ -186,7 +185,7 @@ const Contents = (props: Props): JSX.Element => { { setPage(1); diff --git a/src/pages/contents/folder/[slug].tsx b/src/pages/contents/folder/[slug].tsx index 0302a02..6fd2deb 100644 --- a/src/pages/contents/folder/[slug].tsx +++ b/src/pages/contents/folder/[slug].tsx @@ -20,6 +20,7 @@ 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"; /* * ╭────────╮ @@ -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 = ( - + ); @@ -117,7 +118,7 @@ const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Elemen ) )} renderWhenEmpty={() => <>>} - groupingFunction={() => [langui.folders ?? "Folders"]} + groupingFunction={() => [format("folders")]} /> <>>} - groupingFunction={() => [langui.contents ?? "Contents"]} + groupingFunction={() => [format("contents")]} /> {folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && ( @@ -258,13 +259,13 @@ export const getStaticPaths: GetStaticPaths = async (context) => { */ const NoContentNorFolderMessage = () => { - const langui = useAtomGetter(atoms.localData.langui); + const { format } = useFormat(); return ( - {langui.empty_folder_message} + {format("empty_folder_message")} ); diff --git a/src/pages/library/[slug]/index.tsx b/src/pages/library/[slug]/index.tsx index c6afaf2..338a1a6 100644 --- a/src/pages/library/[slug]/index.tsx +++ b/src/pages/library/[slug]/index.tsx @@ -24,7 +24,6 @@ import { prettyDate, prettyInlineTitle, prettyItemSubType, - prettyItemType, prettyPrice, prettySlug, prettyURL, @@ -54,6 +53,7 @@ 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"; /* * ╭─────────────╮ @@ -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 = ( - + { {item.gallery && item.gallery.data.length > 0 && ( { )} { {item.subitems && item.subitems.data.length > 0 && ( { {item.contents && item.contents.data.length > 0 && ( { @@ -180,7 +180,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { {item.subitem_of?.data[0]?.attributes && ( - {langui.subitem_of} + {format("subitem_of_x", { x: "" })} { <> {item.urls?.length ? ( - {langui.available_at} + {format("available_at")} {filterHasAttributes(item.urls, ["url"] as const).map((url, index) => ( @@ -220,7 +220,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { ))} ) : ( - {langui.item_not_available} + {format("item_not_available")} )} > )} @@ -229,7 +229,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { {item.gallery && item.gallery.data.length > 0 && ( - {langui.gallery} + {format("gallery")} @@ -262,7 +262,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { - {langui.details} + {format("details")} { )}> {item.metadata?.[0] && ( - {langui.type} + {format("type", { count: 1 })} - + {"›"} @@ -281,14 +281,14 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { {item.release_date && ( - {langui.release_date} + {format("release_date")} {prettyDate(item.release_date, router.locale)} )} {item.price && ( - {langui.price} + {format("price")} {prettyPrice( item.price, @@ -299,7 +299,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { {item.price.currency?.data?.attributes?.code !== currency && ( {prettyPrice(item.price, currencies, currency)} ( - {langui.calculated?.toLowerCase()}) + {format("calculated").toLowerCase()}) )} @@ -308,7 +308,9 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { {item.categories && item.categories.data.length > 0 && ( - {langui.categories} + + {format("category", { count: item.categories.data.length })} + {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") )}> - {langui.size} + {format("size")} { "place-items-center" ) )}> - {langui.width}: + {format("width")}: {item.size.width} mm {convertMmToInch(item.size.width)} in @@ -359,7 +361,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { "place-items-center" ) )}> - {langui.height}: + {format("height")}: {item.size.height} mm {convertMmToInch(item.size.height)} in @@ -375,7 +377,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { "place-items-center" ) )}> - {langui.thickness}: + {format("thickness")}: {item.size.thickness} mm {convertMmToInch(item.size.thickness)} in @@ -393,44 +395,53 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { "grid gap-4", cIf(!isContentPanelAtLeast3xl, "place-items-center") )}> - {langui.type_information} + {format("type_information")} {item.metadata?.[0]?.__typename === "ComponentMetadataBooks" && ( <> - - {langui.pages}: - {item.metadata[0].page_count} - + {isDefined(item.metadata[0].page_count) && ( + + {format("page", { count: Infinity })}: + {item.metadata[0].page_count} + + )} - {langui.binding}: + {format("binding")}: {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") : ""} - {langui.page_order}: + {format("page_order")}: {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")} - - {langui.languages}: - {item.metadata[0]?.languages?.data.map((lang) => ( - {lang.attributes?.name} - ))} - + {isDefined(item.metadata[0].languages) && ( + + + {format("language", { + count: item.metadata[0].languages.data.length, + })} + : + + {item.metadata[0].languages.data.map((lang) => ( + {lang.attributes?.name} + ))} + + )} > )} @@ -441,10 +452,12 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { {item.subitems && item.subitems.data.length > 0 && ( - {isVariantSet ? langui.variants : langui.subitems} + + {format(isVariantSet ? "variant" : "subitem", { count: Infinity })} + {hoverable && ( - + )} @@ -493,10 +506,10 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { {item.contents && item.contents.data.length > 0 && ( - {langui.contents} + {format("contents")} {displayOpenScans && ( - + )} @@ -576,7 +589,7 @@ export const getStaticProps: GetStaticProps = async (context) => { const description = getDescription( item.libraryItems.data[0].attributes.descriptions?.[0]?.description, { - [langui.categories ?? "Categories"]: filterHasAttributes( + [langui.category ?? "Categories"]: filterHasAttributes( item.libraryItems.data[0].attributes.categories?.data, ["attributes.short"] ).map((category) => category.attributes.short), @@ -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 && ( )} {isDefined(content) && ( - + )} > ) : ( - langui.content_is_not_available + format("content_is_not_available") )} @@ -747,15 +760,15 @@ const ContentLine = ({ {hasScanSet && ( )} {isDefined(content) && ( - + )} > ) : ( - langui.content_is_not_available + format("content_is_not_available") )} diff --git a/src/pages/library/[slug]/reader.tsx b/src/pages/library/[slug]/reader.tsx index 280ca7b..67bd004 100644 --- a/src/pages/library/[slug]/reader.tsx +++ b/src/pages/library/[slug]/reader.tsx @@ -11,7 +11,7 @@ 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"; @@ -40,6 +40,7 @@ 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"; 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 = ( - + - + - + - + - + {!isWebkit && ( - + )} - {langui.night_reader}: + {format("night_reader")}: - {langui.reading_layout}: + {format("reading_layout")}: 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 = ({ - {langui.quality}: + {format("quality")}: { resetReaderSettings(); @@ -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") } /> @@ -854,7 +855,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps): {content?.data?.attributes && isDefinedAndNotEmpty(content.data.attributes.slug) && ( )} @@ -863,17 +864,15 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps): )} - {langui.status}: - + {format("status")}: + {selectedScan.scanners && selectedScan.scanners.data.length > 0 && ( - {langui.scanners}: + {format("scanners")}: {filterHasAttributes(selectedScan.scanners.data, [ "id", @@ -889,7 +888,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps): {selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && ( - {langui.cleaners}: + {format("cleaners")}: {filterHasAttributes(selectedScan.cleaners.data, [ "id", @@ -905,7 +904,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps): {selectedScan.typesetters && selectedScan.typesetters.data.length > 0 && ( - {langui.typesetters}: + {format("typesetters")}: {filterHasAttributes(selectedScan.typesetters.data, [ "id", @@ -921,7 +920,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps): {isDefinedAndNotEmpty(selectedScan.notes) && ( - + )} diff --git a/src/pages/library/index.tsx b/src/pages/library/index.tsx index eaa7e54..364add9 100644 --- a/src/pages/library/index.tsx +++ b/src/pages/library/index.tsx @@ -25,8 +25,6 @@ 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 +34,7 @@ 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"; /* * ╭─────────────╮ @@ -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 => { { setPage(1); @@ -270,10 +269,10 @@ const Library = (props: Props): JSX.Element => { }} /> - + item.displayedName ?? "")} + options={sortingMethods.map((item) => item.displayedName)} value={sortingMethod} onChange={(newSort) => { setPage(1); @@ -286,7 +285,7 @@ const Library = (props: Props): JSX.Element => { /> - + { @@ -297,7 +296,7 @@ const Library = (props: Props): JSX.Element => { /> - + { @@ -308,7 +307,7 @@ const Library = (props: Props): JSX.Element => { /> - + { @@ -320,7 +319,7 @@ const Library = (props: Props): JSX.Element => { {hoverable && ( - + { @@ -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 => { { setQuery(DEFAULT_FILTERS_STATE.query); diff --git a/src/pages/merch/index.tsx b/src/pages/merch/index.tsx index 4fe5799..51f8993 100644 --- a/src/pages/merch/index.tsx +++ b/src/pages/merch/index.tsx @@ -4,8 +4,7 @@ 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"; +import { useFormat } from "hooks/useFormat"; /* * ╭────────╮ @@ -14,12 +13,16 @@ import { useAtomGetter } from "helpers/atoms"; interface Props extends AppLayoutRequired {} const Merch = (props: Props): JSX.Element => { - const langui = useAtomGetter(atoms.localData.langui); + const { format } = useFormat(); return ( - + } {...props} diff --git a/src/pages/news/[slug].tsx b/src/pages/news/[slug].tsx index e96945b..815a3a0 100644 --- a/src/pages/news/[slug].tsx +++ b/src/pages/news/[slug].tsx @@ -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 ( { - 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 = ( - + { setQuery(name); @@ -134,7 +139,7 @@ const News = ({ ...otherProps }: Props): JSX.Element => { /> {hoverable && ( - + { @@ -147,7 +152,7 @@ const News = ({ ...otherProps }: Props): JSX.Element => { { setQuery(DEFAULT_FILTERS_STATE.query); diff --git a/src/pages/wiki/[slug]/index.tsx b/src/pages/wiki/[slug]/index.tsx index ee66fb1..e23cc28 100644 --- a/src/pages/wiki/[slug]/index.tsx +++ b/src/pages/wiki/[slug]/index.tsx @@ -24,6 +24,7 @@ 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"; /* * ╭────────╮ @@ -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 = ( - + ); @@ -59,7 +60,7 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => { @@ -98,7 +99,9 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => { {page.categories?.data && page.categories.data.length > 0 && ( <> - {langui.categories} + + {format("category", { count: page.categories.data.length })} + {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 && ( <> - {langui.tags} + {format("tags")} {filterHasAttributes(page.tags.data, ["attributes"] as const).map((tag) => ( { {isDefinedAndNotEmpty(selectedTranslation.summary) && ( - {langui.summary} + {format("summary")} {selectedTranslation.summary} )} @@ -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", @@ -244,7 +247,7 @@ export const getStaticProps: GetStaticProps = async (context) => { ] as const).map( (tag) => tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug) ), - [langui.categories ?? "Categories"]: filterHasAttributes( + [langui.category ?? "Categories"]: filterHasAttributes( page.wikiPages.data[0].attributes.categories?.data, ["attributes"] as const ).map((category) => category.attributes.short), diff --git a/src/pages/wiki/chronology.tsx b/src/pages/wiki/chronology.tsx index 483d16d..f4ffc19 100644 --- a/src/pages/wiki/chronology.tsx +++ b/src/pages/wiki/chronology.tsx @@ -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"; @@ -27,8 +26,7 @@ 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"; /* * ╭────────╮ @@ -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 = ( - + @@ -81,7 +79,7 @@ const Chronology = ({ chronologyItems, chronologyEras, ...otherProps }: 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 && ( @@ -334,7 +332,7 @@ export const ChronologyEvent = ({ event, id }: ChronologyEventProps): JSX.Elemen ) : ( - {langui.no_source_warning} + {format("no_source_warning")} )} @@ -354,7 +352,7 @@ export const ChronologyEvent = ({ event, id }: ChronologyEventProps): JSX.Elemen {selectedTranslation.description} )} - {selectedTranslation.note && {`${langui.notes}: ${selectedTranslation.note}`}} + {selectedTranslation.note && {`${format("notes")}: ${selectedTranslation.note}`}} > )} diff --git a/src/pages/wiki/index.tsx b/src/pages/wiki/index.tsx index 415085b..cf92bf2 100644 --- a/src/pages/wiki/index.tsx +++ b/src/pages/wiki/index.tsx @@ -19,12 +19,11 @@ 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"; /* * ╭─────────────╮ @@ -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 => { { setPage(1); @@ -125,7 +124,7 @@ const Wiki = (props: Props): JSX.Element => { /> {hoverable && ( - + { @@ -138,7 +137,7 @@ const Wiki = (props: Props): JSX.Element => { { setPage(1); @@ -150,9 +149,9 @@ const Wiki = (props: Props): JSX.Element => { - {langui.special_pages} + {format("special_pages")} - + );
{langui.no_results_message}
{format("no_results_message")}
{ - 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)) && (
- +
{title}
{`(showing ${SEARCH_LIMIT} out of ${totalHits} results)`}
+ ({format("showing_x_out_of_y_results", { x: SEARCH_LIMIT, y: totalHits })}) +
{langui.status}:
{format("status")}:
{langui.languages}:
{format("language", { count: recorder.languages.data.length })}:
{langui.pronouns}:
{format("pronouns")}:
{`${langui.definition} ${index}`}
{format("definition_x", { x: index })}
{langui.source}:
{format("source")}:
{langui.email_gdpr_notice}
{format("email_gdpr_notice")}
{`${video.channel.data.attributes.subscribers.toLocaleString()} - ${langui.subscribers?.toLowerCase()}`} + ${format("subscribers").toLowerCase()}`}
{video.description}
{langui.source_language}:
{format("source_language")}:
{langui.transcribers}:
{format("transcribers")}:
{langui.translators}:
{format("translators")}:
{langui.proofreaders}:
{format("proofreaders")}:
{langui.notes}:
{format("notes")}:
{langui.source}
{format("source")}
{langui.empty_folder_message}
{format("empty_folder_message")}
{langui.subitem_of}
{format("subitem_of_x", { x: "" })}
{langui.available_at}
{format("available_at")}
{langui.item_not_available}
{format("item_not_available")}
{prettyDate(item.release_date, router.locale)}
{prettyPrice( item.price, @@ -299,7 +299,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { {item.price.currency?.data?.attributes?.code !== currency && (
{prettyPrice(item.price, currencies, currency)} ( - {langui.calculated?.toLowerCase()}) + {format("calculated").toLowerCase()})
{langui.width}:
{format("width")}:
{item.size.width} mm
{convertMmToInch(item.size.width)} in
{langui.height}:
{format("height")}:
{item.size.height} mm
{convertMmToInch(item.size.height)} in
{langui.thickness}:
{format("thickness")}:
{item.size.thickness} mm
{convertMmToInch(item.size.thickness)} in
{langui.pages}:
{item.metadata[0].page_count}
{format("page", { count: Infinity })}:
{langui.binding}:
{format("binding")}:
{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") : ""}
{langui.page_order}:
{format("page_order")}:
{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")}
{lang.attributes?.name}
+ {format("language", { + count: item.metadata[0].languages.data.length, + })} + : +
{langui.night_reader}:
{format("night_reader")}:
{langui.reading_layout}:
{format("reading_layout")}:
{langui.quality}:
{format("quality")}:
{langui.scanners}:
{format("scanners")}:
{langui.cleaners}:
{format("cleaners")}:
{langui.typesetters}:
{format("typesetters")}:
{langui.categories}
+ {format("category", { count: page.categories.data.length })} +
{langui.tags}
{format("tags")}
{langui.summary}
{format("summary")}
{selectedTranslation.summary}
{selectedTranslation.description}
{langui.special_pages}
{format("special_pages")}