feat: add i18n to admin panel (#1326)

Co-authored-by: shikhantmaungs <shinkhantmaungs@gmail.com>
Co-authored-by: Thomas Ghysels <info@thomasg.be>
Co-authored-by: Kokutse Djoguenou <kokutse@Kokutses-MacBook-Pro.local>
Co-authored-by: Christian Gil <47041342+ChrisGV04@users.noreply.github.com>
Co-authored-by: Łukasz Rabiec <lukaszrabiec@gmail.com>
Co-authored-by: Jenny <jennifer.eberlei@gmail.com>
Co-authored-by: Hung Vu <hunghvu2017@gmail.com>
Co-authored-by: Shin Khant Maung <101539335+shinkhantmaungs@users.noreply.github.com>
Co-authored-by: Carlo Brualdi <carlo.brualdi@gmail.com>
Co-authored-by: Ariel Tonglet <ariel.tonglet@gmail.com>
Co-authored-by: Roman Ryzhikov <general+github@ya.ru>
Co-authored-by: maekoya <maekoya@stromatolite.jp>
Co-authored-by: Emilia Trollros <3m1l1a@emiliatrollros.se>
Co-authored-by: Kokutse J Djoguenou <90865585+Julesdj@users.noreply.github.com>
Co-authored-by: Mitch Dries <mitch.dries@gmail.com>

BREAKING CHANGE: If you assigned labels to collections, globals or block names, you need to update your config! Your GraphQL schema and generated Typescript interfaces may have changed. Payload no longer uses labels for code based naming. To prevent breaking changes to your GraphQL API and typescript types in your project, you can assign the below properties to match what Payload previously generated for you from labels.

On Collections
Use `graphQL.singularName`, `graphQL.pluralName` for GraphQL schema names.
Use `typescript.interface` for typescript generation name.

On Globals
Use `graphQL.name` for GraphQL Schema name.
Use `typescript.interface` for typescript generation name.

On Blocks (within Block fields)
Use `graphQL.singularName` for graphQL schema names.
This commit is contained in:
Dan Ribbens
2022-11-18 07:36:30 -05:00
committed by GitHub
parent c49ee15b6a
commit bab34d82f5
279 changed files with 9547 additions and 3242 deletions

View File

@@ -231,6 +231,29 @@ To make use of Payload SCSS variables / mixins to use directly in your own compo
@import '~payload/scss'; @import '~payload/scss';
``` ```
### Getting the current language
When developing custom components you can support multiple languages to be consistent with Payload's i18n support. The best way to do this is to add your translation resources to the [i18n configuration](https://payloadcms.com/docs/configuration/i18n) and import `useTranslation` from `react-i18next` in your components.
For example:
```tsx
import { useTranslation } from 'react-i18next';
const CustomComponent: React.FC = () => {
// highlight-start
const { t, i18n } = useTranslation('namespace1');
// highlight-end
return (
<ul>
<li>{ t('key', { variable: 'value' }) }</li>
<li>{ t('namespace2:key', { variable: 'value' }) }</li>
<li>{ i18n.language }</li>
</ul>
);
};
```
### Getting the current locale ### Getting the current locale
In any custom component you can get the selected locale with the `useLocale` hook. Here is a simple example: In any custom component you can get the selected locale with the `useLocale` hook. Here is a simple example:

View File

@@ -12,19 +12,21 @@ It's often best practice to write your Collections in separate files and then im
## Options ## Options
| Option | Description | | Option | Description |
| ---------------- | -------------| |------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. | | **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | | **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. | | **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). | | **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) | | **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) | | **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) |
| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. | | **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. |
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. | | **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. |
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. | | **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config)| | **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) |
| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints) | | **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints) |
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
*\* An asterisk denotes that a property is required.* *\* An asterisk denotes that a property is required.*

View File

@@ -12,17 +12,19 @@ As with Collection configs, it's often best practice to write your Globals in se
## Options ## Options
| Option | Description | | Option | Description |
| ---------------- | -------------| |--------------------| -------------|
| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. | | **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. |
| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Global. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | | **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Global. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
| **`label`** | Singular label for use in identifying this Global throughout Payload. Auto-generated from slug if not defined. | | **`label`** | Singular label for use in identifying this Global throughout Payload. Auto-generated from slug if not defined. |
| **`description`**| Text or React component to display below the Global header to give editors more information. | | **`description`** | Text or React component to display below the Global header to give editors more information. |
| **`admin`** | Admin-specific configuration. See below for [more detail](/docs/configuration/globals#admin-options). | | **`admin`** | Admin-specific configuration. See below for [more detail](/docs/configuration/globals#admin-options). |
| **`hooks`** | Entry points to "tie in" to collection actions at specific points. [More](/docs/hooks/overview#global-hooks) | | **`hooks`** | Entry points to "tie in" to collection actions at specific points. [More](/docs/hooks/overview#global-hooks) |
| **`access`** | Provide access control functions to define exactly who should be able to do what with this Global. [More](/docs/access-control/overview/#globals) | | **`access`** | Provide access control functions to define exactly who should be able to do what with this Global. [More](/docs/access-control/overview/#globals) |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#globals-config)| | **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#globals-config)|
| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints)| | **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints)|
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
*\* An asterisk denotes that a property is required.* *\* An asterisk denotes that a property is required.*

100
docs/configuration/i18n.mdx Normal file
View File

@@ -0,0 +1,100 @@
---
title: I18n
label: I18n
order: 40
desc: Manage and customize internationalization support in your CMS editor experience
keywords: internationalization, i18n, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
Not only does Payload support managing localized content, it also has internationalization support so that admin users can work in their preferred language. Payload's i18n support is built on top of [i18next](https://www.i18next.com). It comes included by default and can be extended in your config.
While Payload's built-in features come translated, you may want to also translate parts of your project's configuration too. This is possible in places like collections and globals labels and groups, field labels, descriptions and input placeholder text. The admin UI will display all the correct translations you provide based on the user's language.
Here is an example of a simple collection supporting both English and Spanish editors:
```ts
const Articles: CollectionConfig = {
slug: 'articles',
labels: {
singular: {
en: 'Article', es: 'Artículo',
},
plural: {
en: 'Articles', es: 'Artículo',
},
},
admin: {
group: { en: 'Content', es: 'Contenido' },
},
fields: [
{
name: 'title',
type: 'text',
label: {
en: 'Title', es: 'Título',
},
admin: {
placeholder: { en: 'Enter title', es: 'Introduce el título' }
}
},
{
name: 'type',
type: 'radio',
options: [{
value: 'news',
label: { en: 'News', es: 'Noticias' },
}, // etc...
],
},
],
}
```
### Admin UI
The Payload admin panel reads the language settings of a user's browser and display all text in that language, or will fall back to English if the user's language is not yet supported.
After a user logs in, they can change their language selection in the `/account` view.
<Banner>
<strong>Note:</strong><br/>
If there is a language that Payload does not yet support, we accept code <a href="https://github.com/payloadcms/payload/blob/master/contributing.md">contributions</a>.
</Banner>
### Node Express
Payload's backend uses express middleware to set the language on incoming requests before they are handled. This allows backend validation to return error messages in the user's own language or system generated emails to be sent using the correct translation. You can make HTTP requests with the `accept-language` header and Payload will use that language.
Anywhere in your Payload app that you have access to the `req` object, you can access i18next's extensive internationalization features assigned to `req.i18n`. To access text translations you can use `req.t('namespace:key')`.
Read the i18next [API documentation](https://www.i18next.com/overview/api) to learn more.
### Configuration Options
In your Payload config, you can add translations and customize the settings in `i18n`. Payload will use your custom options and merge it with the default, allowing you to override the settings Payload provides.
**Example Payload config extending i18n:**
```ts
import { buildConfig } from 'payload/config'
export default buildConfig({
//...
i18n: {
fallbackLng: 'en', // default
debug: false, // default
resources: {
en: {
custom: { // namespace can be anything you want
key1: 'Translation with {{variable}}', // translation
},
// override existing translation keys
general: {
dashboard: 'Home',
},
},
},
},
//...
});
```
See the i18next [configuration options](https://www.i18next.com/overview/configuration-options) to learn more.

View File

@@ -72,7 +72,7 @@ const ExampleCollection: CollectionConfig = {
{ {
name: 'title', name: 'title',
type: 'text', type: 'text',
} },
{ {
name: 'image', name: 'image',
type: 'upload', type: 'upload',

View File

@@ -22,21 +22,21 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme
### Field config ### Field config
| Option | Description | | Option | Description |
| ---------------- | ----------- | |-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** * | To be used as the property name when stored and retrieved from the database. | | **`name`** * | To be used as the property name when stored and retrieved from the database. |
| **`label`** | Used as a heading in the Admin panel and to name the generated GraphQL type. | | **`label`** | Used as a heading in the Admin panel. |
| **`blocks`** * | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. | | **`blocks`** * | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | | **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-level hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | | **`hooks`** | Provide field-level hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-level access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | | **`access`** | Provide field-level access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin panel. | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin panel. |
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) | | **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. || **`required`** | Require this field to have a value. | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. || **`required`** | Require this field to have a value. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. | | **`labels`** | Customize the block row labels appearing in the Admin dashboard. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | | **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
*\* An asterisk denotes that a property is required.* *\* An asterisk denotes that a property is required.*
@@ -57,13 +57,14 @@ Blocks are defined as separate configs of their own.
Best practice is to define each block config in its own file, and then import them into your Blocks field as necessary. This way each block config can be easily shared between fields. For instance, using the "layout builder" example, you might want to feature a few of the same blocks in a Post collection as well as a Page collection. Abstracting into their own files trivializes their reusability. Best practice is to define each block config in its own file, and then import them into your Blocks field as necessary. This way each block config can be easily shared between fields. For instance, using the "layout builder" example, you might want to feature a few of the same blocks in a Post collection as well as a Page collection. Abstracting into their own files trivializes their reusability.
</Banner> </Banner>
| Option | Description | | Option | Description |
| ---------------- | ----------- | |----------------------------|---------------------------------------------------------------------------------------------------------|
| **`slug`** * | Identifier for this block type. Will be saved on each block as the `blockType` property. | | **`slug`** * | Identifier for this block type. Will be saved on each block as the `blockType` property. |
| **`fields`** * | Array of fields to be stored in this block. | | **`fields`** * | Array of fields to be stored in this block. |
| **`labels`** | Customize the block labels that appear in the Admin dashboard. Also used to name corresponding GraphQL schema types. Auto-generated from slug if not defined. | | **`labels`** | Customize the block labels that appear in the Admin dashboard. Auto-generated from slug if not defined. |
| **`imageURL`** | Provide a custom image thumbnail to help editors identify this block in the Admin UI. | | **`imageURL`** | Provide a custom image thumbnail to help editors identify this block in the Admin UI. |
| **`imageAltText`** | Customize this block's image thumbnail alt text. | | **`imageAltText`** | Customize this block's image thumbnail alt text. |
| **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined |
#### Auto-generated data per block #### Auto-generated data per block

View File

@@ -72,14 +72,15 @@ There are two arguments available to custom validation functions.
1. The value which is currently assigned to the field 1. The value which is currently assigned to the field
2. An optional object with dynamic properties for more complex validation having the following: 2. An optional object with dynamic properties for more complex validation having the following:
| Property | Description | | Property | Description |
| ------------- | -------------| |---------------|--------------------------------------------------------------------------------------------------------------------------|
| `data` | An object of the full collection or global document | | `data` | An object of the full collection or global document. |
| `siblingData` | An object of the document data limited to fields within the same parent to the field | | `siblingData` | An object of the document data limited to fields within the same parent to the field. |
| `operation` | Will be "create" or "update" depending on the UI action or API call | | `operation` | Will be "create" or "update" depending on the UI action or API call. |
| `id` | The value of the collection `id`, will be `undefined` on create request | | `id` | The value of the collection `id`, will be `undefined` on create request. |
| `user` | The currently authenticated user object | | `t` | The function for translating text, [more](/docs/configuration/i18n). |
| `payload` | If the `validate` function is being executed on the server, Payload will be exposed for easily running local operations. | | `user` | The currently authenticated user object. |
| `payload` | If the `validate` function is being executed on the server, Payload will be exposed for easily running local operations. |
Example: Example:
```ts ```ts

View File

@@ -35,10 +35,6 @@ import { CollectionConfig } from 'payload/types';
const PublicUser: CollectionConfig = { const PublicUser: CollectionConfig = {
slug: 'public-users', slug: 'public-users',
auth: true, // Auth is enabled auth: true, // Auth is enabled
labels: {
singular: 'Public User',
plural: 'Public Users',
},
fields: [ fields: [
... ...
], ],

View File

@@ -124,6 +124,9 @@
"graphql-type-json": "^0.3.1", "graphql-type-json": "^0.3.1",
"html-webpack-plugin": "^5.0.0-alpha.14", "html-webpack-plugin": "^5.0.0-alpha.14",
"http-status": "^1.4.2", "http-status": "^1.4.2",
"i18next": "^22.0.1",
"i18next-browser-languagedetector": "^6.1.8",
"i18next-http-middleware": "^3.2.1",
"is-hotkey": "^0.2.0", "is-hotkey": "^0.2.0",
"is-plain-object": "^5.0.0", "is-plain-object": "^5.0.0",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
@@ -167,6 +170,7 @@
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-i18next": "^11.18.6",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-router-navigation-prompt": "^1.9.6", "react-router-navigation-prompt": "^1.9.6",
"react-select": "^3.0.8", "react-select": "^3.0.8",
@@ -237,6 +241,7 @@
"@types/passport-jwt": "^3.0.3", "@types/passport-jwt": "^3.0.3",
"@types/passport-local": "^1.0.33", "@types/passport-local": "^1.0.33",
"@types/pino": "^6.3.4", "@types/pino": "^6.3.4",
"@types/pino-std-serializers": "^4.0.0",
"@types/pluralize": "^0.0.29", "@types/pluralize": "^0.0.29",
"@types/prismjs": "^1.16.2", "@types/prismjs": "^1.16.2",
"@types/prop-types": "^15.7.3", "@types/prop-types": "^15.7.3",

View File

@@ -1,9 +1,19 @@
import qs from 'qs'; import qs from 'qs';
type GetOptions = RequestInit & {
params?: Record<string, unknown>
}
export const requests = { export const requests = {
get: (url: string, params: unknown = {}): Promise<Response> => { get: (url: string, options: GetOptions = { headers: {} }): Promise<Response> => {
const query = qs.stringify(params, { addQueryPrefix: true }); let query = '';
return fetch(`${url}${query}`, { credentials: 'include' }); if (options.params) {
query = qs.stringify(options.params, { addQueryPrefix: true });
}
return fetch(`${url}${query}`, {
credentials: 'include',
headers: options.headers,
});
}, },
post: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => { post: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => {

View File

@@ -2,6 +2,7 @@ import React, { Suspense, lazy, useState, useEffect } from 'react';
import { import {
Route, Switch, withRouter, Redirect, Route, Switch, withRouter, Redirect,
} from 'react-router-dom'; } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from './utilities/Auth'; import { useAuth } from './utilities/Auth';
import { useConfig } from './utilities/Config'; import { useConfig } from './utilities/Config';
import List from './views/collections/List'; import List from './views/collections/List';
@@ -29,6 +30,7 @@ const Account = lazy(() => import('./views/Account'));
const Routes = () => { const Routes = () => {
const [initialized, setInitialized] = useState(null); const [initialized, setInitialized] = useState(null);
const { user, permissions, refreshCookie } = useAuth(); const { user, permissions, refreshCookie } = useAuth();
const { i18n } = useTranslation();
const canAccessAdmin = permissions?.canAccessAdmin; const canAccessAdmin = permissions?.canAccessAdmin;
@@ -54,7 +56,11 @@ const Routes = () => {
const { slug } = userCollection; const { slug } = userCollection;
if (!userCollection.auth.disableLocalStrategy) { if (!userCollection.auth.disableLocalStrategy) {
requests.get(`${routes.api}/${slug}/init`).then((res) => res.json().then((data) => { requests.get(`${routes.api}/${slug}/init`, {
headers: {
'Accept-Language': i18n.language,
},
}).then((res) => res.json().then((data) => {
if (data && 'initialized' in data) { if (data && 'initialized' in data) {
setInitialized(data.initialized); setInitialized(data.initialized);
} }
@@ -62,7 +68,7 @@ const Routes = () => {
} else { } else {
setInitialized(true); setInitialized(true);
} }
}, [routes, userCollection]); }, [i18n.language, routes, userCollection]);
return ( return (
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import Popup from '../Popup'; import Popup from '../Popup';
import More from '../../icons/More'; import More from '../../icons/More';
import Chevron from '../../icons/Chevron'; import Chevron from '../../icons/Chevron';
@@ -19,6 +20,7 @@ export const ArrayAction: React.FC<Props> = ({
duplicateRow, duplicateRow,
removeRow, removeRow,
}) => { }) => {
const { t } = useTranslation('general');
return ( return (
<Popup <Popup
horizontalAlign="center" horizontalAlign="center"
@@ -38,7 +40,7 @@ export const ArrayAction: React.FC<Props> = ({
}} }}
> >
<Chevron /> <Chevron />
Move Up {t('moveUp')}
</button> </button>
)} )}
{index < rowCount - 1 && ( {index < rowCount - 1 && (
@@ -51,7 +53,7 @@ export const ArrayAction: React.FC<Props> = ({
}} }}
> >
<Chevron /> <Chevron />
Move Down {t('moveDown')}
</button> </button>
)} )}
<button <button
@@ -63,7 +65,7 @@ export const ArrayAction: React.FC<Props> = ({
}} }}
> >
<Plus /> <Plus />
Add Below {t('addBelow')}
</button> </button>
<button <button
className={`${baseClass}__action ${baseClass}__duplicate`} className={`${baseClass}__action ${baseClass}__duplicate`}
@@ -74,7 +76,7 @@ export const ArrayAction: React.FC<Props> = ({
}} }}
> >
<Copy /> <Copy />
Duplicate {t('duplicate')}
</button> </button>
<button <button
className={`${baseClass}__action ${baseClass}__remove`} className={`${baseClass}__action ${baseClass}__remove`}
@@ -85,7 +87,7 @@ export const ArrayAction: React.FC<Props> = ({
}} }}
> >
<X /> <X />
Remove {t('remove')}
</button> </button>
</React.Fragment> </React.Fragment>
); );

View File

@@ -1,7 +1,7 @@
import { formatDistance } from 'date-fns';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import { useFormModified, useAllFormFields } from '../../forms/Form/context'; import { useFormModified, useAllFormFields } from '../../forms/Form/context';
import { useLocale } from '../../utilities/Locale'; import { useLocale } from '../../utilities/Locale';
@@ -21,6 +21,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
const modified = useFormModified(); const modified = useFormModified();
const locale = useLocale(); const locale = useLocale();
const { replace } = useHistory(); const { replace } = useHistory();
const { t, i18n } = useTranslation('version');
let interval = 800; let interval = 800;
if (collection?.versions.drafts && collection.versions?.drafts?.autosave) interval = collection.versions.drafts.autosave.interval; if (collection?.versions.drafts && collection.versions?.drafts?.autosave) interval = collection.versions.drafts.autosave.interval;
@@ -42,6 +43,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept-Language': i18n.language,
}, },
body: JSON.stringify({}), body: JSON.stringify({}),
}); });
@@ -54,9 +56,9 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
}, },
}); });
} else { } else {
toast.error('There was a problem while autosaving this document.'); toast.error(t('error:autosaving'));
} }
}, [collection, serverURL, api, admin, locale, replace]); }, [i18n, serverURL, api, collection, locale, replace, admin, t]);
useEffect(() => { useEffect(() => {
// If no ID, but this is used for a collection doc, // If no ID, but this is used for a collection doc,
@@ -98,6 +100,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept-Language': i18n.language,
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
@@ -114,7 +117,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
}; };
autosave(); autosave();
}, [debouncedFields, modified, serverURL, api, collection, global, id, getVersions, locale]); }, [i18n, debouncedFields, modified, serverURL, api, collection, global, id, getVersions, locale]);
useEffect(() => { useEffect(() => {
if (versions?.docs?.[0]) { if (versions?.docs?.[0]) {
@@ -126,12 +129,12 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
return ( return (
<div className={baseClass}> <div className={baseClass}>
{saving && 'Saving...'} {saving && t('saving')}
{(!saving && lastSaved) && ( {(!saving && lastSaved) && (
<React.Fragment> <React.Fragment>
Last saved&nbsp; {t('lastSavedAgo', {
{formatDistance(new Date(), new Date(lastSaved))} distance: Math.round((Number(new Date(lastSaved)) - Number(new Date())) / 1000 / 60),
&nbsp;ago })}
</React.Fragment> </React.Fragment>
)} )}
</div> </div>

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import AnimateHeight from 'react-animate-height'; import AnimateHeight from 'react-animate-height';
import { useTranslation } from 'react-i18next';
import { Props } from './types'; import { Props } from './types';
import { CollapsibleProvider, useCollapsible } from './provider'; import { CollapsibleProvider, useCollapsible } from './provider';
import Chevron from '../../icons/Chevron'; import Chevron from '../../icons/Chevron';
@@ -22,6 +23,7 @@ export const Collapsible: React.FC<Props> = ({
const [collapsedLocal, setCollapsedLocal] = useState(Boolean(initCollapsed)); const [collapsedLocal, setCollapsedLocal] = useState(Boolean(initCollapsed));
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const isNested = useCollapsible(); const isNested = useCollapsible();
const { t } = useTranslation('fields');
const collapsed = typeof collapsedFromProps === 'boolean' ? collapsedFromProps : collapsedLocal; const collapsed = typeof collapsedFromProps === 'boolean' ? collapsedFromProps : collapsedLocal;
@@ -61,7 +63,7 @@ export const Collapsible: React.FC<Props> = ({
}} }}
> >
<span> <span>
Toggle block {t('toggleBlock')}
</span> </span>
</button> </button>
{header && ( {header && (

View File

@@ -1,9 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields'; import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields';
import Pill from '../Pill'; import Pill from '../Pill';
import Plus from '../../icons/Plus'; import Plus from '../../icons/Plus';
import X from '../../icons/X'; import X from '../../icons/X';
import { Props } from './types'; import { Props } from './types';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -17,6 +19,7 @@ const ColumnSelector: React.FC<Props> = (props) => {
} = props; } = props;
const [fields] = useState(() => flattenTopLevelFields(collection.fields, true)); const [fields] = useState(() => flattenTopLevelFields(collection.fields, true));
const { i18n } = useTranslation();
return ( return (
<div className={baseClass}> <div className={baseClass}>
@@ -42,7 +45,7 @@ const ColumnSelector: React.FC<Props> = (props) => {
isEnabled && `${baseClass}__column--active`, isEnabled && `${baseClass}__column--active`,
].filter(Boolean).join(' ')} ].filter(Boolean).join(' ')}
> >
{field.label || field.name} {getTranslation(field.label || field.name, i18n)}
</Pill> </Pill>
); );
})} })}

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import Copy from '../../icons/Copy'; import Copy from '../../icons/Copy';
import Tooltip from '../Tooltip'; import Tooltip from '../Tooltip';
import { Props } from './types'; import { Props } from './types';
@@ -9,12 +10,13 @@ const baseClass = 'copy-to-clipboard';
const CopyToClipboard: React.FC<Props> = ({ const CopyToClipboard: React.FC<Props> = ({
value, value,
defaultMessage = 'copy', defaultMessage,
successMessage = 'copied', successMessage,
}) => { }) => {
const ref = useRef(null); const ref = useRef(null);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const { t } = useTranslation('general');
useEffect(() => { useEffect(() => {
if (copied && !hovered) { if (copied && !hovered) {
@@ -49,8 +51,8 @@ const CopyToClipboard: React.FC<Props> = ({
> >
<Copy /> <Copy />
<Tooltip> <Tooltip>
{copied && successMessage} {copied && (successMessage ?? t('copied'))}
{!copied && defaultMessage} {!copied && (defaultMessage ?? t('copy'))}
</Tooltip> </Tooltip>
<textarea <textarea
readOnly readOnly

View File

@@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { Modal, useModal } from '@faceless-ui/modal'; import { Modal, useModal } from '@faceless-ui/modal';
import { Trans, useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import Button from '../Button'; import Button from '../Button';
import MinimalTemplate from '../../templates/Minimal'; import MinimalTemplate from '../../templates/Minimal';
@@ -9,6 +10,7 @@ import { useForm } from '../../forms/Form/context';
import useTitle from '../../../hooks/useTitle'; import useTitle from '../../../hooks/useTitle';
import { requests } from '../../../api'; import { requests } from '../../../api';
import { Props } from './types'; import { Props } from './types';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -35,14 +37,15 @@ const DeleteDocument: React.FC<Props> = (props) => {
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const { toggleModal } = useModal(); const { toggleModal } = useModal();
const history = useHistory(); const history = useHistory();
const { t, i18n } = useTranslation('general');
const title = useTitle(useAsTitle) || id; const title = useTitle(useAsTitle) || id;
const titleToRender = titleFromProps || title; const titleToRender = titleFromProps || title;
const modalSlug = `delete-${id}`; const modalSlug = `delete-${id}`;
const addDefaultError = useCallback(() => { const addDefaultError = useCallback(() => {
toast.error(`There was an error while deleting ${title}. Please check your connection and try again.`); toast.error(t('error:deletingError', { title }));
}, [title]); }, [t, title]);
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
setDeleting(true); setDeleting(true);
@@ -50,13 +53,14 @@ const DeleteDocument: React.FC<Props> = (props) => {
requests.delete(`${serverURL}${api}/${slug}/${id}`, { requests.delete(`${serverURL}${api}/${slug}/${id}`, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept-Language': i18n.language,
}, },
}).then(async (res) => { }).then(async (res) => {
try { try {
const json = await res.json(); const json = await res.json();
if (res.status < 400) { if (res.status < 400) {
toggleModal(modalSlug); toggleModal(modalSlug);
toast.success(`${singular} "${title}" successfully deleted.`); toast.success(t('titleDeleted', { label: getTranslation(singular, i18n), title }));
return history.push(`${admin}/collections/${slug}`); return history.push(`${admin}/collections/${slug}`);
} }
@@ -72,7 +76,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
return addDefaultError(); return addDefaultError();
} }
}); });
}, [addDefaultError, toggleModal, modalSlug, history, id, singular, slug, title, admin, api, serverURL, setModified]); }, [setModified, serverURL, api, slug, id, toggleModal, modalSlug, t, singular, i18n, title, history, admin, addDefaultError]);
if (id) { if (id) {
return ( return (
@@ -87,24 +91,25 @@ const DeleteDocument: React.FC<Props> = (props) => {
toggleModal(modalSlug); toggleModal(modalSlug);
}} }}
> >
Delete {t('delete')}
</button> </button>
<Modal <Modal
slug={modalSlug} slug={modalSlug}
className={baseClass} className={baseClass}
> >
<MinimalTemplate className={`${baseClass}__template`}> <MinimalTemplate className={`${baseClass}__template`}>
<h1>Confirm deletion</h1> <h1>{t('confirmDeletion')}</h1>
<p> <p>
You are about to delete the <Trans
{' '} i18nKey="aboutToDelete"
{singular} values={{ label: singular, title: titleToRender }}
{' '} t={t}
&quot; >
<strong> aboutToDelete
{titleToRender} <strong>
</strong> {titleToRender}
&quot;. Are you sure? </strong>
</Trans>
</p> </p>
<Button <Button
id="confirm-cancel" id="confirm-cancel"
@@ -112,13 +117,13 @@ const DeleteDocument: React.FC<Props> = (props) => {
type="button" type="button"
onClick={deleting ? undefined : () => toggleModal(modalSlug)} onClick={deleting ? undefined : () => toggleModal(modalSlug)}
> >
Cancel {t('cancel')}
</Button> </Button>
<Button <Button
onClick={deleting ? undefined : handleDelete} onClick={deleting ? undefined : handleDelete}
id="confirm-delete" id="confirm-delete"
> >
{deleting ? 'Deleting...' : 'Confirm'} {deleting ? t('deleting') : t('confirm')}
</Button> </Button>
</MinimalTemplate> </MinimalTemplate>
</Modal> </Modal>

View File

@@ -2,12 +2,14 @@ import React, { useCallback, useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { Modal, useModal } from '@faceless-ui/modal'; import { Modal, useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import { Props } from './types'; import { Props } from './types';
import Button from '../Button'; import Button from '../Button';
import { requests } from '../../../api'; import { requests } from '../../../api';
import { useForm, useFormModified } from '../../forms/Form/context'; import { useForm, useFormModified } from '../../forms/Form/context';
import MinimalTemplate from '../../templates/Minimal'; import MinimalTemplate from '../../templates/Minimal';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -21,6 +23,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
const { serverURL, routes: { api }, localization } = useConfig(); const { serverURL, routes: { api }, localization } = useConfig();
const { routes: { admin } } = useConfig(); const { routes: { admin } } = useConfig();
const [hasClicked, setHasClicked] = useState<boolean>(false); const [hasClicked, setHasClicked] = useState<boolean>(false);
const { t, i18n } = useTranslation('general');
const modalSlug = `duplicate-${id}`; const modalSlug = `duplicate-${id}`;
@@ -34,8 +37,13 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
const create = async (locale = ''): Promise<string | null> => { const create = async (locale = ''): Promise<string | null> => {
const response = await requests.get(`${serverURL}${api}/${slug}/${id}`, { const response = await requests.get(`${serverURL}${api}/${slug}/${id}`, {
locale, params: {
depth: 0, locale,
depth: 0,
},
headers: {
'Accept-Language': i18n.language,
},
}); });
let data = await response.json(); let data = await response.json();
@@ -49,6 +57,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
const result = await requests.post(`${serverURL}${api}/${slug}`, { const result = await requests.post(`${serverURL}${api}/${slug}`, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept-Language': i18n.language,
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
@@ -70,8 +79,13 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
.forEach(async (locale) => { .forEach(async (locale) => {
if (!abort) { if (!abort) {
const res = await requests.get(`${serverURL}${api}/${slug}/${id}`, { const res = await requests.get(`${serverURL}${api}/${slug}/${id}`, {
locale, params: {
depth: 0, locale,
depth: 0,
},
headers: {
'Accept-Language': i18n.language,
},
}); });
let localizedDoc = await res.json(); let localizedDoc = await res.json();
@@ -85,6 +99,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
const patchResult = await requests.patch(`${serverURL}${api}/${slug}/${duplicateID}?locale=${locale}`, { const patchResult = await requests.patch(`${serverURL}${api}/${slug}/${duplicateID}?locale=${locale}`, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept-Language': i18n.language,
}, },
body: JSON.stringify(localizedDoc), body: JSON.stringify(localizedDoc),
}); });
@@ -97,13 +112,17 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
}); });
if (abort) { if (abort) {
// delete the duplicate doc to prevent incomplete // delete the duplicate doc to prevent incomplete
await requests.delete(`${serverURL}${api}/${slug}/${id}`); await requests.delete(`${serverURL}${api}/${slug}/${id}`, {
headers: {
'Accept-Language': i18n.language,
},
});
} }
} else { } else {
duplicateID = await create(); duplicateID = await create();
} }
toast.success(`${collection.labels.singular} successfully duplicated.`, toast.success(t('successfullyDuplicated', { label: getTranslation(collection.labels.singular, i18n) }),
{ autoClose: 3000 }); { autoClose: 3000 });
setModified(false); setModified(false);
@@ -113,7 +132,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
pathname: `${admin}/collections/${slug}/${duplicateID}`, pathname: `${admin}/collections/${slug}/${duplicateID}`,
}); });
}, 10); }, 10);
}, [modified, localization, collection, setModified, toggleModal, modalSlug, serverURL, api, slug, id, push, admin]); }, [modified, localization, t, i18n, collection, setModified, toggleModal, modalSlug, serverURL, api, slug, id, push, admin]);
const confirm = useCallback(async () => { const confirm = useCallback(async () => {
setHasClicked(false); setHasClicked(false);
@@ -128,7 +147,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
className={baseClass} className={baseClass}
onClick={() => handleClick(false)} onClick={() => handleClick(false)}
> >
Duplicate {t('duplicate')}
</Button> </Button>
{modified && hasClicked && ( {modified && hasClicked && (
<Modal <Modal
@@ -136,9 +155,9 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
className={`${baseClass}__modal`} className={`${baseClass}__modal`}
> >
<MinimalTemplate className={`${baseClass}__modal-template`}> <MinimalTemplate className={`${baseClass}__modal-template`}>
<h1>Confirm duplicate</h1> <h1>{t('confirmDuplication')}</h1>
<p> <p>
You have unsaved changes. Would you like to continue to duplicate? {t('unsavedChangesDuplicate')}
</p> </p>
<Button <Button
id="confirm-cancel" id="confirm-cancel"
@@ -146,13 +165,13 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
type="button" type="button"
onClick={() => toggleModal(modalSlug)} onClick={() => toggleModal(modalSlug)}
> >
Cancel {t('cancel')}
</Button> </Button>
<Button <Button
onClick={confirm} onClick={confirm}
id="confirm-duplicate" id="confirm-duplicate"
> >
Duplicate without saving changes {t('duplicateWithoutSaving')}
</Button> </Button>
</MinimalTemplate> </MinimalTemplate>
</Modal> </Modal>

View File

@@ -1,11 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import AnimateHeight from 'react-animate-height'; import AnimateHeight from 'react-animate-height';
import { useTranslation } from 'react-i18next';
import Thumbnail from '../Thumbnail'; import Thumbnail from '../Thumbnail';
import Button from '../Button'; import Button from '../Button';
import Meta from './Meta'; import Meta from './Meta';
import { Props } from './types';
import Chevron from '../../icons/Chevron'; import Chevron from '../../icons/Chevron';
import { Props } from './types';
import './index.scss'; import './index.scss';
@@ -35,6 +35,7 @@ const FileDetails: React.FC<Props> = (props) => {
} = doc; } = doc;
const [moreInfoOpen, setMoreInfoOpen] = useState(false); const [moreInfoOpen, setMoreInfoOpen] = useState(false);
const { t } = useTranslation('upload');
const hasSizes = sizes && Object.keys(sizes)?.length > 0; const hasSizes = sizes && Object.keys(sizes)?.length > 0;
@@ -63,13 +64,13 @@ const FileDetails: React.FC<Props> = (props) => {
> >
{!moreInfoOpen && ( {!moreInfoOpen && (
<React.Fragment> <React.Fragment>
More info {t('moreInfo')}
<Chevron /> <Chevron />
</React.Fragment> </React.Fragment>
)} )}
{moreInfoOpen && ( {moreInfoOpen && (
<React.Fragment> <React.Fragment>
Less info {t('lessInfo')}
<Chevron /> <Chevron />
</React.Fragment> </React.Fragment>
)} )}

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { Modal, useModal } from '@faceless-ui/modal'; import { Modal, useModal } from '@faceless-ui/modal';
import { Trans, useTranslation } from 'react-i18next';
import Button from '../Button'; import Button from '../Button';
import MinimalTemplate from '../../templates/Minimal'; import MinimalTemplate from '../../templates/Minimal';
import { Props } from './types'; import { Props } from './types';
@@ -18,13 +19,14 @@ const GenerateConfirmation: React.FC<Props> = (props) => {
const { id } = useDocumentInfo(); const { id } = useDocumentInfo();
const { toggleModal } = useModal(); const { toggleModal } = useModal();
const { t } = useTranslation('authentication');
const modalSlug = `generate-confirmation-${id}`; const modalSlug = `generate-confirmation-${id}`;
const handleGenerate = () => { const handleGenerate = () => {
setKey(); setKey();
toggleModal(modalSlug); toggleModal(modalSlug);
toast.success('New API Key Generated.', { autoClose: 3000 }); toast.success(t('newAPIKeyGenerated'), { autoClose: 3000 });
highlightField(true); highlightField(true);
}; };
@@ -37,22 +39,22 @@ const GenerateConfirmation: React.FC<Props> = (props) => {
toggleModal(modalSlug); toggleModal(modalSlug);
}} }}
> >
Generate new API key {t('generateNewAPIKey')}
</Button> </Button>
<Modal <Modal
slug={modalSlug} slug={modalSlug}
className={baseClass} className={baseClass}
> >
<MinimalTemplate className={`${baseClass}__template`}> <MinimalTemplate className={`${baseClass}__template`}>
<h1>Confirm Generation</h1> <h1>{t('confirmGeneration')}</h1>
<p> <p>
Generating a new API key will <Trans
{' '} i18nKey="generatingNewAPIKeyWillInvalidate"
<strong>invalidate</strong> t={t}
{' '} >
the previous key. generatingNewAPIKeyWillInvalidate
{' '} <strong>invalidate</strong>
Are you sure you wish to continue? </Trans>
</p> </p>
<Button <Button
@@ -62,12 +64,12 @@ const GenerateConfirmation: React.FC<Props> = (props) => {
toggleModal(modalSlug); toggleModal(modalSlug);
}} }}
> >
Cancel {t('general:cancel')}
</Button> </Button>
<Button <Button
onClick={handleGenerate} onClick={handleGenerate}
> >
Generate {t('generate')}
</Button> </Button>
</MinimalTemplate> </MinimalTemplate>
</Modal> </Modal>

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import AnimateHeight from 'react-animate-height'; import AnimateHeight from 'react-animate-height';
import { FieldAffectingData, fieldAffectsData } from '../../../../fields/config/types'; import { useTranslation } from 'react-i18next';
import { fieldAffectsData } from '../../../../fields/config/types';
import SearchFilter from '../SearchFilter'; import SearchFilter from '../SearchFilter';
import ColumnSelector from '../ColumnSelector'; import ColumnSelector from '../ColumnSelector';
import WhereBuilder from '../WhereBuilder'; import WhereBuilder from '../WhereBuilder';
@@ -10,6 +11,7 @@ import { Props } from './types';
import { useSearchParams } from '../../utilities/SearchParams'; import { useSearchParams } from '../../utilities/SearchParams';
import validateWhereQuery from '../WhereBuilder/validateWhereQuery'; import validateWhereQuery from '../WhereBuilder/validateWhereQuery';
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched'; import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -40,6 +42,7 @@ const ListControls: React.FC<Props> = (props) => {
const [titleField] = useState(() => fields.find((field) => fieldAffectsData(field) && field.name === useAsTitle)); const [titleField] = useState(() => fields.find((field) => fieldAffectsData(field) && field.name === useAsTitle));
const [textFieldsToBeSearched] = useState(getTextFieldsToBeSearched(listSearchableFields, fields)); const [textFieldsToBeSearched] = useState(getTextFieldsToBeSearched(listSearchableFields, fields));
const [visibleDrawer, setVisibleDrawer] = useState<'where' | 'sort' | 'columns'>(shouldInitializeWhereOpened ? 'where' : undefined); const [visibleDrawer, setVisibleDrawer] = useState<'where' | 'sort' | 'columns'>(shouldInitializeWhereOpened ? 'where' : undefined);
const { t, i18n } = useTranslation('general');
return ( return (
<div className={baseClass}> <div className={baseClass}>
@@ -48,7 +51,7 @@ const ListControls: React.FC<Props> = (props) => {
fieldName={titleField && fieldAffectsData(titleField) ? titleField.name : undefined} fieldName={titleField && fieldAffectsData(titleField) ? titleField.name : undefined}
handleChange={handleWhereChange} handleChange={handleWhereChange}
modifySearchQuery={modifySearchQuery} modifySearchQuery={modifySearchQuery}
fieldLabel={titleField && fieldAffectsData(titleField) && titleField.label ? titleField.label : undefined} fieldLabel={(titleField && fieldAffectsData(titleField) && getTranslation(titleField.label || titleField.name, i18n)) ?? undefined}
listSearchableFields={textFieldsToBeSearched} listSearchableFields={textFieldsToBeSearched}
/> />
<div className={`${baseClass}__buttons`}> <div className={`${baseClass}__buttons`}>
@@ -61,7 +64,7 @@ const ListControls: React.FC<Props> = (props) => {
icon="chevron" icon="chevron"
iconStyle="none" iconStyle="none"
> >
Columns {t('columns')}
</Button> </Button>
)} )}
<Button <Button
@@ -71,7 +74,7 @@ const ListControls: React.FC<Props> = (props) => {
icon="chevron" icon="chevron"
iconStyle="none" iconStyle="none"
> >
Filters {t('filters')}
</Button> </Button>
{enableSort && ( {enableSort && (
<Button <Button
@@ -81,7 +84,7 @@ const ListControls: React.FC<Props> = (props) => {
icon="chevron" icon="chevron"
iconStyle="none" iconStyle="none"
> >
Sort {t('sort')}
</Button> </Button>
)} )}
</div> </div>

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import qs from 'qs'; import qs from 'qs';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import { useLocale } from '../../utilities/Locale'; import { useLocale } from '../../utilities/Locale';
import { useSearchParams } from '../../utilities/SearchParams'; import { useSearchParams } from '../../utilities/SearchParams';
@@ -15,6 +16,7 @@ const Localizer: React.FC<Props> = () => {
const { localization } = useConfig(); const { localization } = useConfig();
const locale = useLocale(); const locale = useLocale();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { t } = useTranslation('general');
if (localization) { if (localization) {
const { locales } = localization; const { locales } = localization;
@@ -26,7 +28,7 @@ const Localizer: React.FC<Props> = () => {
button={locale} button={locale}
render={({ close }) => ( render={({ close }) => (
<div> <div>
<span>Locales</span> <span>{t('locales')}</span>
<ul> <ul>
{locales.map((localeOption) => { {locales.map((localeOption) => {
const baseLocaleClass = `${baseClass}__locale`; const baseLocaleClass = `${baseClass}__locale`;

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { NavLink, Link, useHistory } from 'react-router-dom'; import { NavLink, Link, useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import { useAuth } from '../../utilities/Auth'; import { useAuth } from '../../utilities/Auth';
import RenderCustomComponent from '../../utilities/RenderCustomComponent'; import RenderCustomComponent from '../../utilities/RenderCustomComponent';
@@ -10,10 +11,11 @@ import Icon from '../../graphics/Icon';
import Account from '../../graphics/Account'; import Account from '../../graphics/Account';
import Localizer from '../Localizer'; import Localizer from '../Localizer';
import NavGroup from '../NavGroup'; import NavGroup from '../NavGroup';
import Logout from '../Logout';
import { groupNavItems, Group, EntityToGroup, EntityType } from '../../../utilities/groupNavItems'; import { groupNavItems, Group, EntityToGroup, EntityType } from '../../../utilities/groupNavItems';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
import Logout from '../Logout';
const baseClass = 'nav'; const baseClass = 'nav';
@@ -22,6 +24,7 @@ const DefaultNav = () => {
const [menuActive, setMenuActive] = useState(false); const [menuActive, setMenuActive] = useState(false);
const [groups, setGroups] = useState<Group[]>([]); const [groups, setGroups] = useState<Group[]>([]);
const history = useHistory(); const history = useHistory();
const { i18n } = useTranslation('general');
const { const {
collections, collections,
globals, globals,
@@ -31,7 +34,7 @@ const DefaultNav = () => {
admin: { admin: {
components: { components: {
beforeNavLinks, beforeNavLinks,
afterNavLinks afterNavLinks,
}, },
}, },
} = useConfig(); } = useConfig();
@@ -60,8 +63,8 @@ const DefaultNav = () => {
return entityToGroup; return entityToGroup;
}), }),
], permissions)); ], permissions, i18n));
}, [collections, globals, permissions]); }, [collections, globals, permissions, i18n, i18n.language]);
useEffect(() => history.listen(() => { useEffect(() => history.listen(() => {
setMenuActive(false); setMenuActive(false);
@@ -102,13 +105,13 @@ const DefaultNav = () => {
if (type === EntityType.collection) { if (type === EntityType.collection) {
href = `${admin}/collections/${entity.slug}`; href = `${admin}/collections/${entity.slug}`;
entityLabel = entity.labels.plural; entityLabel = getTranslation(entity.labels.plural, i18n);
id = `nav-${entity.slug}`; id = `nav-${entity.slug}`;
} }
if (type === EntityType.global) { if (type === EntityType.global) {
href = `${admin}/globals/${entity.slug}`; href = `${admin}/globals/${entity.slug}`;
entityLabel = entity.label; entityLabel = getTranslation(entity.label, i18n);
id = `nav-global-${entity.slug}`; id = `nav-global-${entity.slug}`;
} }
@@ -137,7 +140,7 @@ const DefaultNav = () => {
> >
<Account /> <Account />
</Link> </Link>
<Logout/> <Logout />
</div> </div>
</nav> </nav>
</div> </div>

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import qs from 'qs'; import qs from 'qs';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from '../../utilities/SearchParams'; import { useSearchParams } from '../../utilities/SearchParams';
import Popup from '../Popup'; import Popup from '../Popup';
import Chevron from '../../icons/Chevron'; import Chevron from '../../icons/Chevron';
@@ -22,6 +23,7 @@ type Props = {
const PerPage: React.FC<Props> = ({ limits = defaultLimits, limit, handleChange, modifySearchParams = true }) => { const PerPage: React.FC<Props> = ({ limits = defaultLimits, limit, handleChange, modifySearchParams = true }) => {
const params = useSearchParams(); const params = useSearchParams();
const history = useHistory(); const history = useHistory();
const { t } = useTranslation('general');
return ( return (
<div className={baseClass}> <div className={baseClass}>
@@ -29,9 +31,7 @@ const PerPage: React.FC<Props> = ({ limits = defaultLimits, limit, handleChange,
horizontalAlign="right" horizontalAlign="right"
button={( button={(
<strong> <strong>
Per Page: {t('perPage', { limit })}
{' '}
{limit}
<Chevron /> <Chevron />
</strong> </strong>
)} )}

View File

@@ -5,7 +5,6 @@ import PopupButton from './PopupButton';
import useIntersect from '../../../hooks/useIntersect'; import useIntersect from '../../../hooks/useIntersect';
import './index.scss'; import './index.scss';
import useThrottledEffect from '../../../hooks/useThrottledEffect';
const baseClass = 'popup'; const baseClass = 'popup';

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../utilities/Auth'; import { useAuth } from '../../utilities/Auth';
import Button from '../Button'; import Button from '../Button';
import { Props } from './types'; import { Props } from './types';
@@ -18,6 +19,7 @@ const PreviewButton: React.FC<Props> = (props) => {
const locale = useLocale(); const locale = useLocale();
const { token } = useAuth(); const { token } = useAuth();
const { t } = useTranslation('version');
useEffect(() => { useEffect(() => {
if (generatePreviewURL && typeof generatePreviewURL === 'function') { if (generatePreviewURL && typeof generatePreviewURL === 'function') {
@@ -44,7 +46,7 @@ const PreviewButton: React.FC<Props> = (props) => {
url={url} url={url}
newTab newTab
> >
Preview {t('preview')}
</Button> </Button>
); );
} }

View File

@@ -1,4 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import FormSubmit from '../../forms/Submit'; import FormSubmit from '../../forms/Submit';
import { Props } from './types'; import { Props } from './types';
import { useDocumentInfo } from '../../utilities/DocumentInfo'; import { useDocumentInfo } from '../../utilities/DocumentInfo';
@@ -8,6 +9,7 @@ const Publish: React.FC<Props> = () => {
const { unpublishedVersions, publishedDoc } = useDocumentInfo(); const { unpublishedVersions, publishedDoc } = useDocumentInfo();
const { submit } = useForm(); const { submit } = useForm();
const modified = useFormModified(); const modified = useFormModified();
const { t } = useTranslation('version');
const hasNewerVersions = unpublishedVersions?.totalDocs > 0; const hasNewerVersions = unpublishedVersions?.totalDocs > 0;
const canPublish = modified || hasNewerVersions || !publishedDoc; const canPublish = modified || hasNewerVersions || !publishedDoc;
@@ -26,7 +28,7 @@ const Publish: React.FC<Props> = () => {
onClick={publish} onClick={publish}
disabled={!canPublish} disabled={!canPublish}
> >
Publish changes {t('publishChanges')}
</FormSubmit> </FormSubmit>
); );
}; };

View File

@@ -12,9 +12,11 @@ import {
SortEndHandler, SortEndHandler,
SortableHandle, SortableHandle,
} from 'react-sortable-hoc'; } from 'react-sortable-hoc';
import { useTranslation } from 'react-i18next';
import { arrayMove } from '../../../../utilities/arrayMove'; import { arrayMove } from '../../../../utilities/arrayMove';
import { Props, Value } from './types'; import { Props, Value } from './types';
import Chevron from '../../icons/Chevron'; import Chevron from '../../icons/Chevron';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -64,6 +66,8 @@ const ReactSelect: React.FC<Props> = (props) => {
filterOption = undefined, filterOption = undefined,
} = props; } = props;
const { i18n } = useTranslation();
const classes = [ const classes = [
className, className,
'react-select', 'react-select',
@@ -92,7 +96,7 @@ const ReactSelect: React.FC<Props> = (props) => {
// small fix for https://github.com/clauderic/react-sortable-hoc/pull/352: // small fix for https://github.com/clauderic/react-sortable-hoc/pull/352:
getHelperDimensions={({ node }) => node.getBoundingClientRect()} getHelperDimensions={({ node }) => node.getBoundingClientRect()}
// react-select props: // react-select props:
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
{...props} {...props}
value={value as Value[]} value={value as Value[]}
onChange={onChange} onChange={onChange}
@@ -117,7 +121,7 @@ const ReactSelect: React.FC<Props> = (props) => {
return ( return (
<Select <Select
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
captureMenuScroll captureMenuScroll
{...props} {...props}
value={value} value={value}

View File

@@ -1,4 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import FormSubmit from '../../forms/Submit'; import FormSubmit from '../../forms/Submit';
import { useForm, useFormModified } from '../../forms/Form/context'; import { useForm, useFormModified } from '../../forms/Form/context';
@@ -15,6 +16,7 @@ const SaveDraft: React.FC = () => {
const { collection, global, id } = useDocumentInfo(); const { collection, global, id } = useDocumentInfo();
const modified = useFormModified(); const modified = useFormModified();
const locale = useLocale(); const locale = useLocale();
const { t } = useTranslation('version');
const canSaveDraft = modified; const canSaveDraft = modified;
@@ -50,7 +52,7 @@ const SaveDraft: React.FC = () => {
onClick={saveDraft} onClick={saveDraft}
disabled={!canSaveDraft} disabled={!canSaveDraft}
> >
Save draft {t('saveDraft')}
</FormSubmit> </FormSubmit>
); );
}; };

View File

@@ -1,11 +1,13 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import queryString from 'qs'; import queryString from 'qs';
import { useTranslation } from 'react-i18next';
import { Props } from './types'; import { Props } from './types';
import Search from '../../icons/Search'; import Search from '../../icons/Search';
import useDebounce from '../../../hooks/useDebounce'; import useDebounce from '../../../hooks/useDebounce';
import { useSearchParams } from '../../utilities/SearchParams'; import { useSearchParams } from '../../utilities/SearchParams';
import { Where, WhereField } from '../../../../types'; import { Where, WhereField } from '../../../../types';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -22,11 +24,12 @@ const SearchFilter: React.FC<Props> = (props) => {
const params = useSearchParams(); const params = useSearchParams();
const history = useHistory(); const history = useHistory();
const { t, i18n } = useTranslation('general');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [previousSearch, setPreviousSearch] = useState(''); const [previousSearch, setPreviousSearch] = useState('');
const placeholder = useRef(`Search by ${fieldLabel}`); const placeholder = useRef(t('searchBy', { label: getTranslation(fieldLabel, i18n) }));
const debouncedSearch = useDebounce(search, 300); const debouncedSearch = useDebounce(search, 300);
@@ -78,12 +81,12 @@ const SearchFilter: React.FC<Props> = (props) => {
if (listSearchableFields?.length > 0) { if (listSearchableFields?.length > 0) {
placeholder.current = listSearchableFields.reduce<string>((prev, curr, i) => { placeholder.current = listSearchableFields.reduce<string>((prev, curr, i) => {
if (i === listSearchableFields.length - 1) { if (i === listSearchableFields.length - 1) {
return `${prev} or ${curr.label || curr.name}`; return `${prev} ${t('or')} ${getTranslation(curr.label || curr.name, i18n)}`;
} }
return `${prev}, ${curr.label || curr.name}`; return `${prev}, ${getTranslation(curr.label || curr.name, i18n)}`;
}, placeholder.current); }, placeholder.current);
} }
}, [listSearchableFields]); }, [t, listSearchableFields, i18n]);
return ( return (
<div className={baseClass}> <div className={baseClass}>

View File

@@ -1,12 +1,14 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import queryString from 'qs'; import queryString from 'qs';
import { useTranslation } from 'react-i18next';
import { Props } from './types'; import { Props } from './types';
import Chevron from '../../icons/Chevron'; import Chevron from '../../icons/Chevron';
import Button from '../Button'; import Button from '../Button';
import { useSearchParams } from '../../utilities/SearchParams';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
import { useSearchParams } from '../../utilities/SearchParams';
const baseClass = 'sort-column'; const baseClass = 'sort-column';
@@ -16,6 +18,7 @@ const SortColumn: React.FC<Props> = (props) => {
} = props; } = props;
const params = useSearchParams(); const params = useSearchParams();
const history = useHistory(); const history = useHistory();
const { i18n } = useTranslation();
const { sort } = params; const { sort } = params;
@@ -39,7 +42,7 @@ const SortColumn: React.FC<Props> = (props) => {
return ( return (
<div className={baseClass}> <div className={baseClass}>
<span className={`${baseClass}__label`}>{label}</span> <span className={`${baseClass}__label`}>{getTranslation(label, i18n)}</span>
{!disable && ( {!disable && (
<span className={`${baseClass}__buttons`}> <span className={`${baseClass}__buttons`}>
<Button <Button

View File

@@ -1,5 +1,5 @@
export type Props = { export type Props = {
label: string, label: Record<string, string> | string,
name: string, name: string,
disable?: boolean, disable?: boolean,
} }

View File

@@ -1,11 +1,13 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import queryString from 'qs'; import queryString from 'qs';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Props } from './types'; import { Props } from './types';
import ReactSelect from '../ReactSelect'; import ReactSelect from '../ReactSelect';
import sortableFieldTypes from '../../../../fields/sortableFieldTypes'; import sortableFieldTypes from '../../../../fields/sortableFieldTypes';
import { useSearchParams } from '../../utilities/SearchParams'; import { useSearchParams } from '../../utilities/SearchParams';
import { fieldAffectsData } from '../../../../fields/config/types'; import { fieldAffectsData } from '../../../../fields/config/types';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -22,19 +24,20 @@ const SortComplex: React.FC<Props> = (props) => {
const history = useHistory(); const history = useHistory();
const params = useSearchParams(); const params = useSearchParams();
const { t, i18n } = useTranslation('general');
const [sortFields] = useState(() => collection.fields.reduce((fields, field) => { const [sortFields] = useState(() => collection.fields.reduce((fields, field) => {
if (fieldAffectsData(field) && sortableFieldTypes.indexOf(field.type) > -1) { if (fieldAffectsData(field) && sortableFieldTypes.indexOf(field.type) > -1) {
return [ return [
...fields, ...fields,
{ label: field.label, value: field.name }, { label: getTranslation(field.label || field.name, i18n), value: field.name },
]; ];
} }
return fields; return fields;
}, [])); }, []));
const [sortField, setSortField] = useState(sortFields[0]); const [sortField, setSortField] = useState(sortFields[0]);
const [sortOrder, setSortOrder] = useState({ label: 'Descending', value: '-' }); const [sortOrder, setSortOrder] = useState({ label: t('descending'), value: '-' });
useEffect(() => { useEffect(() => {
if (sortField?.value) { if (sortField?.value) {
@@ -59,7 +62,7 @@ const SortComplex: React.FC<Props> = (props) => {
<div className={`${baseClass}__wrap`}> <div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__select`}> <div className={`${baseClass}__select`}>
<div className={`${baseClass}__label`}> <div className={`${baseClass}__label`}>
Column to Sort {t('columnToSort')}
</div> </div>
<ReactSelect <ReactSelect
value={sortField} value={sortField}
@@ -69,7 +72,7 @@ const SortComplex: React.FC<Props> = (props) => {
</div> </div>
<div className={`${baseClass}__select`}> <div className={`${baseClass}__select`}>
<div className={`${baseClass}__label`}> <div className={`${baseClass}__label`}>
Order {t('order')}
</div> </div>
<ReactSelect <ReactSelect
value={sortOrder} value={sortOrder}

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { Modal, useModal } from '@faceless-ui/modal'; import { Modal, useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import { Props } from './types'; import { Props } from './types';
import { useDocumentInfo } from '../../utilities/DocumentInfo'; import { useDocumentInfo } from '../../utilities/DocumentInfo';
@@ -16,12 +17,23 @@ import './index.scss';
const baseClass = 'status'; const baseClass = 'status';
const Status: React.FC<Props> = () => { const Status: React.FC<Props> = () => {
const { publishedDoc, unpublishedVersions, collection, global, id, getVersions } = useDocumentInfo(); const {
publishedDoc,
unpublishedVersions,
collection,
global,
id,
getVersions,
} = useDocumentInfo();
const { toggleModal } = useModal(); const { toggleModal } = useModal();
const { serverURL, routes: { api } } = useConfig(); const {
serverURL,
routes: { api },
} = useConfig();
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const { reset: resetForm } = useForm(); const { reset: resetForm } = useForm();
const locale = useLocale(); const locale = useLocale();
const { t, i18n } = useTranslation('version');
const unPublishModalSlug = `confirm-un-publish-${id}`; const unPublishModalSlug = `confirm-un-publish-${id}`;
const revertModalSlug = `confirm-revert-${id}`; const revertModalSlug = `confirm-revert-${id}`;
@@ -29,11 +41,11 @@ const Status: React.FC<Props> = () => {
let statusToRender; let statusToRender;
if (unpublishedVersions?.docs?.length > 0 && publishedDoc) { if (unpublishedVersions?.docs?.length > 0 && publishedDoc) {
statusToRender = 'Changed'; statusToRender = t('changed');
} else if (!publishedDoc) { } else if (!publishedDoc) {
statusToRender = 'Draft'; statusToRender = t('draft');
} else if (publishedDoc && unpublishedVersions?.docs?.length <= 1) { } else if (publishedDoc && unpublishedVersions?.docs?.length <= 1) {
statusToRender = 'Published'; statusToRender = t('published');
} }
const performAction = useCallback(async (action: 'revert' | 'unpublish') => { const performAction = useCallback(async (action: 'revert' | 'unpublish') => {
@@ -65,6 +77,7 @@ const Status: React.FC<Props> = () => {
const res = await requests[method](url, { const res = await requests[method](url, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept-Language': i18n.language,
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
@@ -88,7 +101,7 @@ const Status: React.FC<Props> = () => {
toast.success(json.message); toast.success(json.message);
getVersions(); getVersions();
} else { } else {
toast.error('There was a problem while un-publishing this document.'); toast.error(t('unPublishingDocument'));
} }
setProcessing(false); setProcessing(false);
@@ -99,7 +112,7 @@ const Status: React.FC<Props> = () => {
if (action === 'unpublish') { if (action === 'unpublish') {
toggleModal(unPublishModalSlug); toggleModal(unPublishModalSlug);
} }
}, [collection, global, publishedDoc, serverURL, api, id, locale, resetForm, getVersions, toggleModal, revertModalSlug, unPublishModalSlug]); }, [collection, global, publishedDoc, serverURL, api, id, i18n, locale, resetForm, getVersions, t, toggleModal, revertModalSlug, unPublishModalSlug]);
if (statusToRender) { if (statusToRender) {
return ( return (
@@ -114,26 +127,26 @@ const Status: React.FC<Props> = () => {
className={`${baseClass}__action`} className={`${baseClass}__action`}
buttonStyle="none" buttonStyle="none"
> >
Unpublish {t('unpublish')}
</Button> </Button>
<Modal <Modal
slug={unPublishModalSlug} slug={unPublishModalSlug}
className={`${baseClass}__modal`} className={`${baseClass}__modal`}
> >
<MinimalTemplate className={`${baseClass}__modal-template`}> <MinimalTemplate className={`${baseClass}__modal-template`}>
<h1>Confirm unpublish</h1> <h1>{t('confirmUnpublish')}</h1>
<p>You are about to unpublish this document. Are you sure?</p> <p>{t('aboutToUnpublish')}</p>
<Button <Button
buttonStyle="secondary" buttonStyle="secondary"
type="button" type="button"
onClick={processing ? undefined : () => toggleModal(unPublishModalSlug)} onClick={processing ? undefined : () => toggleModal(unPublishModalSlug)}
> >
Cancel {t('general:cancel')}
</Button> </Button>
<Button <Button
onClick={processing ? undefined : () => performAction('unpublish')} onClick={processing ? undefined : () => performAction('unpublish')}
> >
{processing ? 'Unpublishing...' : 'Confirm'} {t(processing ? 'unpublishing' : 'general:confirm')}
</Button> </Button>
</MinimalTemplate> </MinimalTemplate>
</Modal> </Modal>
@@ -147,26 +160,26 @@ const Status: React.FC<Props> = () => {
className={`${baseClass}__action`} className={`${baseClass}__action`}
buttonStyle="none" buttonStyle="none"
> >
Revert to published {t('revertToPublished')}
</Button> </Button>
<Modal <Modal
slug={revertModalSlug} slug={revertModalSlug}
className={`${baseClass}__modal`} className={`${baseClass}__modal`}
> >
<MinimalTemplate className={`${baseClass}__modal-template`}> <MinimalTemplate className={`${baseClass}__modal-template`}>
<h1>Confirm revert to saved</h1> <h1>{t('confirmRevertToSaved')}</h1>
<p>You are about to revert this document&apos;s changes to its published state. Are you sure?</p> <p>{t('aboutToRevertToPublished')}</p>
<Button <Button
buttonStyle="secondary" buttonStyle="secondary"
type="button" type="button"
onClick={processing ? undefined : () => toggleModal(revertModalSlug)} onClick={processing ? undefined : () => toggleModal(revertModalSlug)}
> >
Cancel {t('general:published')}
</Button> </Button>
<Button <Button
onClick={processing ? undefined : () => performAction('revert')} onClick={processing ? undefined : () => performAction('revert')}
> >
{processing ? 'Reverting...' : 'Confirm'} {t(processing ? 'reverting' : 'general:confirm')}
</Button> </Button>
</MinimalTemplate> </MinimalTemplate>
</Modal> </Modal>

View File

@@ -2,8 +2,10 @@ import React, {
useState, createContext, useContext, useState, createContext, useContext,
} from 'react'; } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Chevron from '../../icons/Chevron'; import Chevron from '../../icons/Chevron';
import { Context as ContextType } from './types'; import { Context as ContextType } from './types';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -26,7 +28,8 @@ const StepNavProvider: React.FC<{children?: React.ReactNode}> = ({ children }) =
const useStepNav = (): ContextType => useContext(Context); const useStepNav = (): ContextType => useContext(Context);
const StepNav: React.FC = () => { const StepNav: React.FC = () => {
const dashboardLabel = <span>Dashboard</span>; const { t, i18n } = useTranslation();
const dashboardLabel = <span>{t('general:dashboard')}</span>;
const { stepNav } = useStepNav(); const { stepNav } = useStepNav();
return ( return (
@@ -40,7 +43,7 @@ const StepNav: React.FC = () => {
) )
: dashboardLabel} : dashboardLabel}
{stepNav.map((item, i) => { {stepNav.map((item, i) => {
const StepLabel = <span key={i}>{item.label}</span>; const StepLabel = <span key={i}>{getTranslation(item.label, i18n)}</span>;
const Step = stepNav.length === i + 1 const Step = stepNav.length === i + 1
? StepLabel ? StepLabel

View File

@@ -1,5 +1,5 @@
export type StepNavItem = { export type StepNavItem = {
label: string label: Record<string, string> | string
url?: string url?: string
} }

View File

@@ -1,5 +1,6 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/click-events-have-key-events */
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { Props } from './types'; import { Props } from './types';
import Thumbnail from '../Thumbnail'; import Thumbnail from '../Thumbnail';
@@ -15,6 +16,8 @@ const UploadCard: React.FC<Props> = (props) => {
collection, collection,
} = props; } = props;
const { t } = useTranslation('general');
const classes = [ const classes = [
baseClass, baseClass,
className, className,
@@ -32,7 +35,7 @@ const UploadCard: React.FC<Props> = (props) => {
collection={collection} collection={collection}
/> />
<div className={`${baseClass}__filename`}> <div className={`${baseClass}__filename`}>
{typeof doc?.filename === 'string' ? doc?.filename : '[Untitled]'} {typeof doc?.filename === 'string' ? doc?.filename : `[${t('untitled')}]`}
</div> </div>
</div> </div>
); );

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import Button from '../Button'; import Button from '../Button';
import { Props } from './types'; import { Props } from './types';
@@ -14,6 +15,7 @@ const baseClass = 'versions-count';
const VersionsCount: React.FC<Props> = ({ collection, global, id }) => { const VersionsCount: React.FC<Props> = ({ collection, global, id }) => {
const { routes: { admin } } = useConfig(); const { routes: { admin } } = useConfig();
const { versions, publishedDoc, unpublishedVersions } = useDocumentInfo(); const { versions, publishedDoc, unpublishedVersions } = useDocumentInfo();
const { t } = useTranslation('version');
// Doc status could come from three places: // Doc status could come from three places:
// 1. the newest unpublished version (a draft) // 1. the newest unpublished version (a draft)
@@ -44,7 +46,7 @@ const VersionsCount: React.FC<Props> = ({ collection, global, id }) => {
return ( return (
<div className={baseClass}> <div className={baseClass}>
{versionCount === 0 && 'No versions found'} {versionCount === 0 && t('versionCount_none')}
{versionCount > 0 && ( {versionCount > 0 && (
<Button <Button
className={`${baseClass}__button`} className={`${baseClass}__button`}
@@ -52,12 +54,7 @@ const VersionsCount: React.FC<Props> = ({ collection, global, id }) => {
el="link" el="link"
to={versionsURL} to={versionsURL}
> >
{versionCount} {t('versionCount', { count: versionCount })}
{' '}
version
{versionCount > 1 && 's'}
{' '}
found
</Button> </Button>
)} )}
</div> </div>

View File

@@ -1,8 +1,11 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { Props, isComponent } from './types'; import { Props, isComponent } from './types';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
const ViewDescription: React.FC<Props> = (props) => { const ViewDescription: React.FC<Props> = (props) => {
const { i18n } = useTranslation();
const { const {
description, description,
} = props; } = props;
@@ -17,7 +20,7 @@ const ViewDescription: React.FC<Props> = (props) => {
<div <div
className="view-description" className="view-description"
> >
{typeof description === 'function' ? description() : description} {typeof description === 'function' ? description() : getTranslation(description, i18n) }
</div> </div>
); );
} }

View File

@@ -4,7 +4,7 @@ export type DescriptionFunction = () => string
export type DescriptionComponent = React.ComponentType<any> export type DescriptionComponent = React.ComponentType<any>
type Description = string | DescriptionFunction | DescriptionComponent type Description = Record<string, string> | string | DescriptionFunction | DescriptionComponent
export type Props = { export type Props = {
description?: Description description?: Description

View File

@@ -1,18 +1,22 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { Props } from './types'; import { Props } from './types';
import './index.scss'; import './index.scss';
const baseClass = 'condition-value-number'; const baseClass = 'condition-value-number';
const NumberField: React.FC<Props> = ({ onChange, value }) => ( const NumberField: React.FC<Props> = ({ onChange, value }) => {
<input const { t } = useTranslation('general');
placeholder="Enter a value" return (
className={baseClass} <input
type="number" placeholder={t('enterAValue')}
onChange={(e) => onChange(e.target.value)} className={baseClass}
value={value} type="number"
/> onChange={(e) => onChange(e.target.value)}
); value={value}
/>
);
};
export default NumberField; export default NumberField;

View File

@@ -1,4 +1,5 @@
import React, { useReducer, useState, useCallback, useEffect } from 'react'; import React, { useReducer, useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../utilities/Config'; import { useConfig } from '../../../../utilities/Config';
import { Props, Option, ValueWithRelation, GetResults } from './types'; import { Props, Option, ValueWithRelation, GetResults } from './types';
import optionsReducer from './optionsReducer'; import optionsReducer from './optionsReducer';
@@ -32,11 +33,12 @@ const RelationshipField: React.FC<Props> = (props) => {
const [errorLoading, setErrorLoading] = useState(''); const [errorLoading, setErrorLoading] = useState('');
const [hasLoadedFirstOptions, setHasLoadedFirstOptions] = useState(false); const [hasLoadedFirstOptions, setHasLoadedFirstOptions] = useState(false);
const debouncedSearch = useDebounce(search, 300); const debouncedSearch = useDebounce(search, 300);
const { t, i18n } = useTranslation('general');
const addOptions = useCallback((data, relation) => { const addOptions = useCallback((data, relation) => {
const collection = collections.find((coll) => coll.slug === relation); const collection = collections.find((coll) => coll.slug === relation);
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection }); dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection, i18n });
}, [collections, hasMultipleRelations]); }, [collections, hasMultipleRelations, i18n]);
const getResults = useCallback<GetResults>(async ({ const getResults = useCallback<GetResults>(async ({
lastFullyLoadedRelation: lastFullyLoadedRelationArg, lastFullyLoadedRelation: lastFullyLoadedRelationArg,
@@ -60,10 +62,15 @@ const RelationshipField: React.FC<Props> = (props) => {
const fieldToSearch = collection?.admin?.useAsTitle || 'id'; const fieldToSearch = collection?.admin?.useAsTitle || 'id';
const searchParam = searchArg ? `&where[${fieldToSearch}][like]=${searchArg}` : ''; const searchParam = searchArg ? `&where[${fieldToSearch}][like]=${searchArg}` : '';
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`, { credentials: 'include' }); const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
});
if (response.ok) { if (response.ok) {
const data: PaginatedDocs<any> = await response.json(); const data: PaginatedDocs = await response.json();
if (data.docs.length > 0) { if (data.docs.length > 0) {
resultsFetched += data.docs.length; resultsFetched += data.docs.length;
addOptions(data, relation); addOptions(data, relation);
@@ -80,12 +87,12 @@ const RelationshipField: React.FC<Props> = (props) => {
} }
} }
} else { } else {
setErrorLoading('An error has occurred.'); setErrorLoading(t('errors:unspecific'));
} }
} }
}, Promise.resolve()); }, Promise.resolve());
} }
}, [addOptions, api, collections, serverURL, errorLoading, relationTo]); }, [i18n, relationTo, errorLoading, collections, serverURL, api, addOptions, t]);
const findOptionsByValue = useCallback((): Option | Option[] => { const findOptionsByValue = useCallback((): Option | Option[] => {
if (value) { if (value) {
@@ -152,16 +159,21 @@ const RelationshipField: React.FC<Props> = (props) => {
const addOptionByID = useCallback(async (id, relation) => { const addOptionByID = useCallback(async (id, relation) => {
if (!errorLoading && id !== 'null') { if (!errorLoading && id !== 'null') {
const response = await fetch(`${serverURL}${api}/${relation}/${id}?depth=0`, { credentials: 'include' }); const response = await fetch(`${serverURL}${api}/${relation}/${id}?depth=0`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
});
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
addOptions({ docs: [data] }, relation); addOptions({ docs: [data] }, relation);
} else { } else {
console.error(`There was a problem loading the document with ID of ${id}.`); console.error(t('error:loadingDocument', { id }));
} }
} }
}, [addOptions, api, errorLoading, serverURL]); }, [i18n, addOptions, api, errorLoading, serverURL, t]);
// /////////////////////////// // ///////////////////////////
// Get results when search input changes // Get results when search input changes
@@ -171,13 +183,14 @@ const RelationshipField: React.FC<Props> = (props) => {
dispatchOptions({ dispatchOptions({
type: 'CLEAR', type: 'CLEAR',
required: true, required: true,
i18n,
}); });
setHasLoadedFirstOptions(true); setHasLoadedFirstOptions(true);
setLastLoadedPage(1); setLastLoadedPage(1);
setLastFullyLoadedRelation(-1); setLastFullyLoadedRelation(-1);
getResults({ search: debouncedSearch }); getResults({ search: debouncedSearch });
}, [getResults, debouncedSearch, relationTo]); }, [getResults, debouncedSearch, relationTo, i18n]);
// /////////////////////////// // ///////////////////////////
// Format options once first options have been retrieved // Format options once first options have been retrieved
@@ -224,7 +237,7 @@ const RelationshipField: React.FC<Props> = (props) => {
<div className={classes}> <div className={classes}>
{!errorLoading && ( {!errorLoading && (
<ReactSelect <ReactSelect
placeholder="Select a value" placeholder={t('selectValue')}
onInputChange={handleInputChange} onInputChange={handleInputChange}
onChange={(selected) => { onChange={(selected) => {
if (hasMany) { if (hasMany) {

View File

@@ -1,4 +1,5 @@
import { Option, Action } from './types'; import { Option, Action } from './types';
import { getTranslation } from '../../../../../../utilities/getTranslation';
const reduceToIDs = (options) => options.reduce((ids, option) => { const reduceToIDs = (options) => options.reduce((ids, option) => {
if (option.options) { if (option.options) {
@@ -17,11 +18,11 @@ const reduceToIDs = (options) => options.reduce((ids, option) => {
const optionsReducer = (state: Option[], action: Action): Option[] => { const optionsReducer = (state: Option[], action: Action): Option[] => {
switch (action.type) { switch (action.type) {
case 'CLEAR': { case 'CLEAR': {
return action.required ? [] : [{ value: 'null', label: 'None' }]; return action.required ? [] : [{ value: 'null', label: action.i18n.t('general:none') }];
} }
case 'ADD': { case 'ADD': {
const { hasMultipleRelations, collection, relation, data } = action; const { hasMultipleRelations, collection, relation, data, i18n } = action;
const labelKey = collection.admin.useAsTitle || 'id'; const labelKey = collection.admin.useAsTitle || 'id';
@@ -47,7 +48,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
} }
const newOptions = [...state]; const newOptions = [...state];
const optionsToAddTo = newOptions.find((optionGroup) => optionGroup.label === collection.labels.plural); const optionsToAddTo = newOptions.find((optionGroup) => optionGroup.label === getTranslation(collection.labels.plural, i18n));
const newSubOptions = data.docs.reduce((docs, doc) => { const newSubOptions = data.docs.reduce((docs, doc) => {
if (loadedIDs.indexOf(doc.id) === -1) { if (loadedIDs.indexOf(doc.id) === -1) {
@@ -73,7 +74,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
]; ];
} else { } else {
newOptions.push({ newOptions.push({
label: collection.labels.plural, label: getTranslation(collection.labels.plural, i18n),
options: newSubOptions, options: newSubOptions,
value: undefined, value: undefined,
}); });

View File

@@ -1,3 +1,4 @@
import i18n from 'i18next';
import { RelationshipField } from '../../../../../../fields/config/types'; import { RelationshipField } from '../../../../../../fields/config/types';
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types'; import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
import { PaginatedDocs } from '../../../../../../mongoose/types'; import { PaginatedDocs } from '../../../../../../mongoose/types';
@@ -17,6 +18,7 @@ export type Option = {
type CLEAR = { type CLEAR = {
type: 'CLEAR' type: 'CLEAR'
required: boolean required: boolean
i18n: typeof i18n
} }
type ADD = { type ADD = {
@@ -25,6 +27,7 @@ type ADD = {
relation: string relation: string
hasMultipleRelations: boolean hasMultipleRelations: boolean
collection: SanitizedCollectionConfig collection: SanitizedCollectionConfig
i18n: typeof i18n
} }
export type Action = CLEAR | ADD export type Action = CLEAR | ADD

View File

@@ -1,6 +1,7 @@
import React, { useState, useReducer } from 'react'; import React, { useState, useReducer } from 'react';
import queryString from 'qs'; import queryString from 'qs';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Props } from './types'; import { Props } from './types';
import useThrottledEffect from '../../../hooks/useThrottledEffect'; import useThrottledEffect from '../../../hooks/useThrottledEffect';
import Button from '../Button'; import Button from '../Button';
@@ -11,15 +12,16 @@ import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields';
import { useSearchParams } from '../../utilities/SearchParams'; import { useSearchParams } from '../../utilities/SearchParams';
import validateWhereQuery from './validateWhereQuery'; import validateWhereQuery from './validateWhereQuery';
import { Where } from '../../../../types'; import { Where } from '../../../../types';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
const baseClass = 'where-builder'; const baseClass = 'where-builder';
const reduceFields = (fields) => flattenTopLevelFields(fields).reduce((reduced, field) => { const reduceFields = (fields, i18n) => flattenTopLevelFields(fields).reduce((reduced, field) => {
if (typeof fieldTypes[field.type] === 'object') { if (typeof fieldTypes[field.type] === 'object') {
const formattedField = { const formattedField = {
label: field.label, label: getTranslation(field.label || field.name, i18n),
value: field.name, value: field.name,
...fieldTypes[field.type], ...fieldTypes[field.type],
props: { props: {
@@ -50,6 +52,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
const history = useHistory(); const history = useHistory();
const params = useSearchParams(); const params = useSearchParams();
const { t, i18n } = useTranslation('general');
const [conditions, dispatchConditions] = useReducer(reducer, params.where, (whereFromSearch) => { const [conditions, dispatchConditions] = useReducer(reducer, params.where, (whereFromSearch) => {
if (modifySearchQuery && validateWhereQuery(whereFromSearch)) { if (modifySearchQuery && validateWhereQuery(whereFromSearch)) {
@@ -59,7 +62,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
return []; return [];
}); });
const [reducedFields] = useState(() => reduceFields(collection.fields)); const [reducedFields] = useState(() => reduceFields(collection.fields, i18n));
useThrottledEffect(() => { useThrottledEffect(() => {
const currentParams = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 10 }) as { where: Where }; const currentParams = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 10 }) as { where: Where };
@@ -104,18 +107,14 @@ const WhereBuilder: React.FC<Props> = (props) => {
{conditions.length > 0 && ( {conditions.length > 0 && (
<React.Fragment> <React.Fragment>
<div className={`${baseClass}__label`}> <div className={`${baseClass}__label`}>
Filter {t('filterWhere', { label: getTranslation(plural, i18n) }) }
{' '}
{plural}
{' '}
where
</div> </div>
<ul className={`${baseClass}__or-filters`}> <ul className={`${baseClass}__or-filters`}>
{conditions.map((or, orIndex) => ( {conditions.map((or, orIndex) => (
<li key={orIndex}> <li key={orIndex}>
{orIndex !== 0 && ( {orIndex !== 0 && (
<div className={`${baseClass}__label`}> <div className={`${baseClass}__label`}>
Or {t('or')}
</div> </div>
)} )}
<ul className={`${baseClass}__and-filters`}> <ul className={`${baseClass}__and-filters`}>
@@ -123,7 +122,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
<li key={andIndex}> <li key={andIndex}>
{andIndex !== 0 && ( {andIndex !== 0 && (
<div className={`${baseClass}__label`}> <div className={`${baseClass}__label`}>
And {t('and')}
</div> </div>
)} )}
<Condition <Condition
@@ -148,13 +147,13 @@ const WhereBuilder: React.FC<Props> = (props) => {
iconStyle="with-border" iconStyle="with-border"
onClick={() => dispatchConditions({ type: 'add', field: reducedFields[0].value })} onClick={() => dispatchConditions({ type: 'add', field: reducedFields[0].value })}
> >
Or {t('or')}
</Button> </Button>
</React.Fragment> </React.Fragment>
)} )}
{conditions.length === 0 && ( {conditions.length === 0 && (
<div className={`${baseClass}__no-filters`}> <div className={`${baseClass}__no-filters`}>
<div className={`${baseClass}__label`}>No filters set</div> <div className={`${baseClass}__label`}>{t('noFiltersSet')}</div>
<Button <Button
className={`${baseClass}__add-first-filter`} className={`${baseClass}__add-first-filter`}
icon="plus" icon="plus"
@@ -163,7 +162,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
iconStyle="with-border" iconStyle="with-border"
onClick={() => dispatchConditions({ type: 'add', field: reducedFields[0].value })} onClick={() => dispatchConditions({ type: 'add', field: reducedFields[0].value })}
> >
Add filter {t('addFilter')}
</Button> </Button>
</div> </div>
)} )}

View File

@@ -9,7 +9,7 @@ const baseClass = 'field-error';
const Error: React.FC<Props> = (props) => { const Error: React.FC<Props> = (props) => {
const { const {
showError = false, showError = false,
message = 'Please complete this field.', message,
} = props; } = props;
if (showError) { if (showError) {

View File

@@ -1,4 +1,4 @@
export type Props = { export type Props = {
showError?: boolean showError?: boolean
message?: string message: string
} }

View File

@@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { Props, isComponent } from './types'; import { Props, isComponent } from './types';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
const baseClass = 'field-description'; const baseClass = 'field-description';
@@ -11,6 +13,7 @@ const FieldDescription: React.FC<Props> = (props) => {
value, value,
} = props; } = props;
const { i18n } = useTranslation();
if (isComponent(description)) { if (isComponent(description)) {
const Description = description; const Description = description;
@@ -25,7 +28,7 @@ const FieldDescription: React.FC<Props> = (props) => {
className, className,
].filter(Boolean).join(' ')} ].filter(Boolean).join(' ')}
> >
{typeof description === 'function' ? description({ value }) : description} {typeof description === 'function' ? description({ value }) : getTranslation(description, i18n)}
</div> </div>
); );
} }

View File

@@ -4,7 +4,7 @@ export type DescriptionFunction = (value?: unknown) => string
export type DescriptionComponent = React.ComponentType<{ value: unknown }> export type DescriptionComponent = React.ComponentType<{ value: unknown }>
export type Description = string | DescriptionFunction | DescriptionComponent export type Description = Record<string, string> | string | DescriptionFunction | DescriptionComponent
export type Props = { export type Props = {
description?: Description description?: Description

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import ObjectID from 'bson-objectid'; import ObjectID from 'bson-objectid';
import type { TFunction } from 'i18next';
import { User } from '../../../../../auth'; import { User } from '../../../../../auth';
import { import {
NonPresentationalField, NonPresentationalField,
@@ -23,6 +24,7 @@ type Args = {
operation: 'create' | 'update' operation: 'create' | 'update'
data: Data data: Data
fullData: Data fullData: Data
t: TFunction
} }
export const addFieldStatePromise = async ({ export const addFieldStatePromise = async ({
@@ -37,6 +39,7 @@ export const addFieldStatePromise = async ({
fieldPromises, fieldPromises,
id, id,
operation, operation,
t,
}: Args): Promise<void> => { }: Args): Promise<void> => {
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
const fieldState: Field = { const fieldState: Field = {
@@ -63,6 +66,7 @@ export const addFieldStatePromise = async ({
siblingData: data, siblingData: data,
id, id,
operation, operation,
t,
}); });
} }
@@ -97,6 +101,7 @@ export const addFieldStatePromise = async ({
id, id,
locale, locale,
operation, operation,
t,
}); });
}); });
@@ -152,6 +157,7 @@ export const addFieldStatePromise = async ({
operation, operation,
fieldPromises, fieldPromises,
id, id,
t,
}); });
} }
}); });
@@ -183,6 +189,7 @@ export const addFieldStatePromise = async ({
path: `${path}${field.name}.`, path: `${path}${field.name}.`,
locale, locale,
user, user,
t,
}); });
break; break;
@@ -212,6 +219,7 @@ export const addFieldStatePromise = async ({
id, id,
locale, locale,
operation, operation,
t,
}); });
} else if (field.type === 'tabs') { } else if (field.type === 'tabs') {
field.tabs.forEach((tab) => { field.tabs.forEach((tab) => {
@@ -227,6 +235,7 @@ export const addFieldStatePromise = async ({
id, id,
locale, locale,
operation, operation,
t,
}); });
}); });
} }

View File

@@ -1,3 +1,4 @@
import type { TFunction } from 'i18next';
import { User } from '../../../../../auth'; import { User } from '../../../../../auth';
import { Field as FieldSchema } from '../../../../../fields/config/types'; import { Field as FieldSchema } from '../../../../../fields/config/types';
import { Fields, Data } from '../types'; import { Fields, Data } from '../types';
@@ -11,6 +12,7 @@ type Args = {
id?: string | number, id?: string | number,
operation?: 'create' | 'update' operation?: 'create' | 'update'
locale: string locale: string
t: TFunction
} }
const buildStateFromSchema = async (args: Args): Promise<Fields> => { const buildStateFromSchema = async (args: Args): Promise<Fields> => {
@@ -21,6 +23,7 @@ const buildStateFromSchema = async (args: Args): Promise<Fields> => {
id, id,
operation, operation,
locale, locale,
t,
} = args; } = args;
if (fieldSchema) { if (fieldSchema) {
@@ -39,6 +42,7 @@ const buildStateFromSchema = async (args: Args): Promise<Fields> => {
data: fullData, data: fullData,
fullData, fullData,
parentPassesCondition: true, parentPassesCondition: true,
t,
}); });
await Promise.all(fieldPromises); await Promise.all(fieldPromises);

View File

@@ -1,3 +1,4 @@
import type { TFunction } from 'i18next';
import { User } from '../../../../../auth'; import { User } from '../../../../../auth';
import { import {
Field as FieldSchema, Field as FieldSchema,
@@ -18,6 +19,7 @@ type Args = {
fieldPromises: Promise<void>[] fieldPromises: Promise<void>[]
id: string | number id: string | number
operation: 'create' | 'update' operation: 'create' | 'update'
t: TFunction
} }
export const iterateFields = ({ export const iterateFields = ({
@@ -32,6 +34,7 @@ export const iterateFields = ({
fieldPromises, fieldPromises,
id, id,
state, state,
t,
}: Args): void => { }: Args): void => {
fields.forEach((field) => { fields.forEach((field) => {
const initialData = data; const initialData = data;
@@ -51,6 +54,7 @@ export const iterateFields = ({
field, field,
passesCondition, passesCondition,
data, data,
t,
})); }));
} }
}); });

View File

@@ -6,6 +6,7 @@ import isDeepEqual from 'deep-equal';
import { serialize } from 'object-to-formdata'; import { serialize } from 'object-to-formdata';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../utilities/Auth'; import { useAuth } from '../../utilities/Auth';
import { useLocale } from '../../utilities/Locale'; import { useLocale } from '../../utilities/Locale';
import { useDocumentInfo } from '../../utilities/DocumentInfo'; import { useDocumentInfo } from '../../utilities/DocumentInfo';
@@ -46,6 +47,7 @@ const Form: React.FC<Props> = (props) => {
const history = useHistory(); const history = useHistory();
const locale = useLocale(); const locale = useLocale();
const { t, i18n } = useTranslation('general');
const { refreshCookie, user } = useAuth(); const { refreshCookie, user } = useAuth();
const { id } = useDocumentInfo(); const { id } = useDocumentInfo();
const operation = useOperation(); const operation = useOperation();
@@ -90,6 +92,7 @@ const Form: React.FC<Props> = (props) => {
user, user,
id, id,
operation, operation,
t,
}); });
} }
@@ -110,7 +113,7 @@ const Form: React.FC<Props> = (props) => {
} }
return isValid; return isValid;
}, [contextRef, id, user, operation, dispatchFields]); }, [contextRef, id, user, operation, t, dispatchFields]);
const submit = useCallback(async (options: SubmitOptions = {}, e): Promise<void> => { const submit = useCallback(async (options: SubmitOptions = {}, e): Promise<void> => {
const { const {
@@ -142,7 +145,7 @@ const Form: React.FC<Props> = (props) => {
// If not valid, prevent submission // If not valid, prevent submission
if (!isValid) { if (!isValid) {
toast.error('Please correct invalid fields.'); toast.error(t('error:correctInvalidFields'));
setProcessing(false); setProcessing(false);
return; return;
@@ -164,6 +167,9 @@ const Form: React.FC<Props> = (props) => {
try { try {
const res = await requests[methodToUse.toLowerCase()](actionToUse, { const res = await requests[methodToUse.toLowerCase()](actionToUse, {
body: formData, body: formData,
headers: {
'Accept-Language': i18n.language,
},
}); });
setModified(false); setModified(false);
@@ -206,7 +212,7 @@ const Form: React.FC<Props> = (props) => {
history.push(destination); history.push(destination);
} else if (!disableSuccessStatus) { } else if (!disableSuccessStatus) {
toast.success(json.message || 'Submission successful.', { autoClose: 3000 }); toast.success(json.message || t('submissionSuccessful'), { autoClose: 3000 });
} }
} else { } else {
contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form
@@ -262,13 +268,13 @@ const Form: React.FC<Props> = (props) => {
}); });
nonFieldErrors.forEach((err) => { nonFieldErrors.forEach((err) => {
toast.error(err.message || 'An unknown error occurred.'); toast.error(err.message || t('error:unknown'));
}); });
return; return;
} }
const message = errorMessages[res.status] || 'An unknown error occurred.'; const message = errorMessages[res.status] || t('error:unknown');
toast.error(message); toast.error(message);
} }
@@ -291,6 +297,8 @@ const Form: React.FC<Props> = (props) => {
onSubmit, onSubmit,
onSuccess, onSuccess,
redirect, redirect,
t,
i18n,
waitForAutocomplete, waitForAutocomplete,
]); ]);
@@ -325,10 +333,10 @@ const Form: React.FC<Props> = (props) => {
}, [contextRef]); }, [contextRef]);
const reset = useCallback(async (fieldSchema: Field[], data: unknown) => { const reset = useCallback(async (fieldSchema: Field[], data: unknown) => {
const state = await buildStateFromSchema({ fieldSchema, data, user, id, operation, locale }); const state = await buildStateFromSchema({ fieldSchema, data, user, id, operation, locale, t });
contextRef.current = { ...initContextState } as FormContextType; contextRef.current = { ...initContextState } as FormContextType;
dispatchFields({ type: 'REPLACE_STATE', state }); dispatchFields({ type: 'REPLACE_STATE', state });
}, [id, user, operation, locale, dispatchFields]); }, [id, user, operation, locale, t, dispatchFields]);
contextRef.current.submit = submit; contextRef.current.submit = submit;
contextRef.current.getFields = getFields; contextRef.current.getFields = getFields;

View File

@@ -1,11 +1,15 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { Props } from './types'; import { Props } from './types';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
const Label: React.FC<Props> = (props) => { const Label: React.FC<Props> = (props) => {
const { const {
label, required = false, htmlFor, label, required = false, htmlFor,
} = props; } = props;
const { i18n } = useTranslation();
if (label) { if (label) {
return ( return (
@@ -13,7 +17,7 @@ const Label: React.FC<Props> = (props) => {
htmlFor={htmlFor} htmlFor={htmlFor}
className="field-label" className="field-label"
> >
{label} { getTranslation(label, i18n) }
{required && <span className="required">*</span>} {required && <span className="required">*</span>}
</label> </label>
); );

View File

@@ -1,5 +1,5 @@
export type Props = { export type Props = {
label?: string | false | JSX.Element label?: Record<string, string> | string | false | JSX.Element
required?: boolean required?: boolean
htmlFor?: string htmlFor?: string
} }

View File

@@ -1,9 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import RenderCustomComponent from '../../utilities/RenderCustomComponent'; import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import useIntersect from '../../../hooks/useIntersect'; import useIntersect from '../../../hooks/useIntersect';
import { Props } from './types'; import { Props } from './types';
import { fieldAffectsData, fieldIsPresentationalOnly } from '../../../../fields/config/types'; import { fieldAffectsData, fieldIsPresentationalOnly } from '../../../../fields/config/types';
import { useOperation } from '../../utilities/OperationProvider'; import { useOperation } from '../../utilities/OperationProvider';
import { getTranslation } from '../../../../utilities/getTranslation';
const baseClass = 'render-fields'; const baseClass = 'render-fields';
@@ -23,6 +25,7 @@ const RenderFields: React.FC<Props> = (props) => {
indexPath: incomingIndexPath, indexPath: incomingIndexPath,
} = props; } = props;
const { t, i18n } = useTranslation('general');
const [hasRendered, setHasRendered] = useState(Boolean(forceRender)); const [hasRendered, setHasRendered] = useState(Boolean(forceRender));
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions); const [intersectionRef, entry] = useIntersect(intersectionObserverOptions);
const operation = useOperation(); const operation = useOperation();
@@ -107,7 +110,7 @@ const RenderFields: React.FC<Props> = (props) => {
className="missing-field" className="missing-field"
key={fieldIndex} key={fieldIndex}
> >
{`No matched field found for "${field.label}"`} {t('error:noMatchedField', { label: fieldAffectsData(field) ? getTranslation(field.label || field.name, i18n) : field.path })}
</div> </div>
); );
} }

View File

@@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { isComponent, Props } from './types'; import { isComponent, Props } from './types';
import { useWatchForm } from '../Form/context'; import { useWatchForm } from '../Form/context';
import { getTranslation } from '../../../../utilities/getTranslation';
const baseClass = 'row-label'; const baseClass = 'row-label';
@@ -27,6 +29,7 @@ const RowLabelContent: React.FC<Omit<Props, 'className'>> = (props) => {
rowNumber, rowNumber,
} = props; } = props;
const { i18n } = useTranslation();
const { getDataByPath, getSiblingData } = useWatchForm(); const { getDataByPath, getSiblingData } = useWatchForm();
const collapsibleData = getSiblingData(path); const collapsibleData = getSiblingData(path);
const arrayData = getDataByPath(path); const arrayData = getDataByPath(path);
@@ -49,7 +52,7 @@ const RowLabelContent: React.FC<Omit<Props, 'className'>> = (props) => {
data, data,
path, path,
index: rowNumber, index: rowNumber,
}) : label} }) : getTranslation(label, i18n)}
</React.Fragment> </React.Fragment>
); );
}; };

View File

@@ -18,7 +18,7 @@ export type RowLabelFunction = (args: RowLabelArgs) => string
export type RowLabelComponent = React.ComponentType<RowLabelArgs> export type RowLabelComponent = React.ComponentType<RowLabelArgs>
export type RowLabel = string | RowLabelFunction | RowLabelComponent export type RowLabel = string | Record<string, string> | RowLabelFunction | RowLabelComponent
export function isComponent(label: RowLabel): label is RowLabelComponent { export function isComponent(label: RowLabel): label is RowLabelComponent {
return React.isValidElement(label); return React.isValidElement(label);

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useReducer } from 'react'; import React, { useCallback, useEffect, useReducer } from 'react';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../../utilities/Auth'; import { useAuth } from '../../../utilities/Auth';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import Button from '../../../elements/Button'; import Button from '../../../elements/Button';
@@ -25,6 +26,7 @@ import HiddenInput from '../HiddenInput';
import { RowLabel } from '../../RowLabel'; import { RowLabel } from '../../RowLabel';
import './index.scss'; import './index.scss';
import { getTranslation } from '../../../../../utilities/getTranslation';
const baseClass = 'array-field'; const baseClass = 'array-field';
@@ -52,14 +54,6 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const path = pathFromProps || name; const path = pathFromProps || name;
// Handle labeling for Arrays, Global Arrays, and Blocks
const getLabels = (p: Props) => {
if (p?.labels) return p.labels;
if (p?.label) return { singular: p.label, plural: undefined };
return { singular: 'Row', plural: 'Rows' };
};
const labels = getLabels(props);
// eslint-disable-next-line react/destructuring-assignment // eslint-disable-next-line react/destructuring-assignment
const label = props?.label ?? props?.labels?.singular; const label = props?.label ?? props?.labels?.singular;
@@ -74,6 +68,16 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const { id } = useDocumentInfo(); const { id } = useDocumentInfo();
const locale = useLocale(); const locale = useLocale();
const operation = useOperation(); const operation = useOperation();
const { t, i18n } = useTranslation('fields');
// Handle labeling for Arrays, Global Arrays, and Blocks
const getLabels = (p: Props) => {
if (p?.labels) return p.labels;
if (p?.label) return { singular: p.label, plural: undefined };
return { singular: t('row'), plural: t('rows') };
};
const labels = getLabels(props);
const { dispatchFields, setModified } = formContext; const { dispatchFields, setModified } = formContext;
@@ -93,7 +97,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
}); });
const addRow = useCallback(async (rowIndex: number) => { const addRow = useCallback(async (rowIndex: number) => {
const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale }); const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale, t });
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path }); dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path });
dispatchRows({ type: 'ADD', rowIndex }); dispatchRows({ type: 'ADD', rowIndex });
setModified(true); setModified(true);
@@ -101,7 +105,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
setTimeout(() => { setTimeout(() => {
scrollToID(`${path}-row-${rowIndex + 1}`); scrollToID(`${path}-row-${rowIndex + 1}`);
}, 0); }, 0);
}, [dispatchRows, dispatchFields, fields, path, operation, id, user, locale, setModified]); }, [dispatchRows, dispatchFields, fields, path, operation, id, user, locale, setModified, t]);
const duplicateRow = useCallback(async (rowIndex: number) => { const duplicateRow = useCallback(async (rowIndex: number) => {
dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path }); dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path });
@@ -220,7 +224,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
</div> </div>
<header className={`${baseClass}__header`}> <header className={`${baseClass}__header`}>
<div className={`${baseClass}__header-wrap`}> <div className={`${baseClass}__header-wrap`}>
<h3>{label}</h3> <h3>{getTranslation(label || name, i18n)}</h3>
<ul className={`${baseClass}__header-actions`}> <ul className={`${baseClass}__header-actions`}>
<li> <li>
<button <button
@@ -228,7 +232,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
onClick={() => toggleCollapseAll(true)} onClick={() => toggleCollapseAll(true)}
className={`${baseClass}__header-action`} className={`${baseClass}__header-action`}
> >
Collapse All {t('collapseAll')}
</button> </button>
</li> </li>
<li> <li>
@@ -237,12 +241,13 @@ const ArrayFieldType: React.FC<Props> = (props) => {
onClick={() => toggleCollapseAll(false)} onClick={() => toggleCollapseAll(false)}
className={`${baseClass}__header-action`} className={`${baseClass}__header-action`}
> >
Show All {t('showAll')}
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
<FieldDescription <FieldDescription
className={`field-description-${path.replace(/\./gi, '__')}`}
value={value} value={value}
description={description} description={description}
/> />
@@ -319,19 +324,18 @@ const ArrayFieldType: React.FC<Props> = (props) => {
})} })}
{(rows.length < minRows || (required && rows.length === 0)) && ( {(rows.length < minRows || (required && rows.length === 0)) && (
<Banner type="error"> <Banner type="error">
This field requires at least {t('validation:requiresAtLeast', {
{' '} count: minRows,
{minRows label: getTranslation(minRows
? `${minRows} ${labels.plural}` ? labels.plural
: `1 ${labels.singular}`} : labels.singular,
i18n) || t(minRows > 1 ? 'general:row' : 'general:rows'),
})}
</Banner> </Banner>
)} )}
{(rows.length === 0 && readOnly) && ( {(rows.length === 0 && readOnly) && (
<Banner> <Banner>
This field has no {t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
{' '}
{labels.plural}
.
</Banner> </Banner>
)} )}
{provided.placeholder} {provided.placeholder}
@@ -347,7 +351,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
iconStyle="with-border" iconStyle="with-border"
iconPosition="left" iconPosition="left"
> >
{`Add ${labels.singular}`} {t('addLabel', { label: getTranslation(labels.singular, i18n) })}
</Button> </Button>
</div> </div>
)} )}

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import SearchIcon from '../../../../../graphics/Search'; import SearchIcon from '../../../../../graphics/Search';
import './index.scss'; import './index.scss';
@@ -7,6 +8,7 @@ const baseClass = 'block-search';
const BlockSearch: React.FC<{ setSearchTerm: (term: string) => void }> = (props) => { const BlockSearch: React.FC<{ setSearchTerm: (term: string) => void }> = (props) => {
const { setSearchTerm } = props; const { setSearchTerm } = props;
const { t } = useTranslation('fields');
const handleChange = (e) => { const handleChange = (e) => {
setSearchTerm(e.target.value); setSearchTerm(e.target.value);
@@ -16,7 +18,7 @@ const BlockSearch: React.FC<{ setSearchTerm: (term: string) => void }> = (props)
<div className={baseClass}> <div className={baseClass}>
<input <input
className={`${baseClass}__input`} className={`${baseClass}__input`}
placeholder="Search for a block" placeholder={t('searchForBlock')}
onChange={handleChange} onChange={handleChange}
/> />
<SearchIcon /> <SearchIcon />

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { getTranslation } from '../../../../../../../utilities/getTranslation';
import DefaultBlockImage from '../../../../../graphics/DefaultBlockImage'; import DefaultBlockImage from '../../../../../graphics/DefaultBlockImage';
import { Props } from './types'; import { Props } from './types';
@@ -12,6 +13,8 @@ const BlockSelection: React.FC<Props> = (props) => {
addRow, addRowIndex, block, close, addRow, addRowIndex, block, close,
} = props; } = props;
const { i18n } = useTranslation();
const { const {
labels, slug, imageURL, imageAltText, labels, slug, imageURL, imageAltText,
} = block; } = block;
@@ -38,7 +41,7 @@ const BlockSelection: React.FC<Props> = (props) => {
) )
: <DefaultBlockImage />} : <DefaultBlockImage />}
</div> </div>
<div className={`${baseClass}__label`}>{labels.singular}</div> <div className={`${baseClass}__label`}>{getTranslation(labels.singular, i18n)}</div>
</button> </button>
); );
}; };

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import useField from '../../../useField'; import useField from '../../../useField';
import { Props } from './types'; import { Props } from './types';
@@ -10,6 +11,7 @@ const SectionTitle: React.FC<Props> = (props) => {
const { path, readOnly } = props; const { path, readOnly } = props;
const { value, setValue } = useField({ path }); const { value, setValue } = useField({ path });
const { t } = useTranslation('general');
const classes = [ const classes = [
baseClass, baseClass,
@@ -24,7 +26,7 @@ const SectionTitle: React.FC<Props> = (props) => {
className={`${baseClass}__input`} className={`${baseClass}__input`}
id={path} id={path}
value={value as string || ''} value={value as string || ''}
placeholder="Untitled" placeholder={t('untitled')}
type="text" type="text"
name={path} name={path}
onChange={(e) => { onChange={(e) => {

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useReducer, useState } from 'react'; import React, { useCallback, useEffect, useReducer, useState } from 'react';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../../utilities/Auth'; import { useAuth } from '../../../utilities/Auth';
import { usePreferences } from '../../../utilities/Preferences'; import { usePreferences } from '../../../utilities/Preferences';
import { useLocale } from '../../../utilities/Locale'; import { useLocale } from '../../../utilities/Locale';
@@ -27,23 +28,23 @@ import SectionTitle from './SectionTitle';
import Pill from '../../../elements/Pill'; import Pill from '../../../elements/Pill';
import { scrollToID } from '../../../../utilities/scrollToID'; import { scrollToID } from '../../../../utilities/scrollToID';
import HiddenInput from '../HiddenInput'; import HiddenInput from '../HiddenInput';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
const baseClass = 'blocks-field'; const baseClass = 'blocks-field';
const labelDefaults = {
singular: 'Block',
plural: 'Blocks',
};
const BlocksField: React.FC<Props> = (props) => { const BlocksField: React.FC<Props> = (props) => {
const { t, i18n } = useTranslation('fields');
const { const {
label, label,
name, name,
path: pathFromProps, path: pathFromProps,
blocks, blocks,
labels = labelDefaults, labels = {
singular: t('block'),
plural: t('blocks'),
},
fieldTypes, fieldTypes,
maxRows, maxRows,
minRows, minRows,
@@ -98,7 +99,7 @@ const BlocksField: React.FC<Props> = (props) => {
const addRow = useCallback(async (rowIndex: number, blockType: string) => { const addRow = useCallback(async (rowIndex: number, blockType: string) => {
const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType); const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType);
const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user, locale }); const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user, locale, t });
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path, blockType }); dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path, blockType });
dispatchRows({ type: 'ADD', rowIndex, blockType }); dispatchRows({ type: 'ADD', rowIndex, blockType });
setModified(true); setModified(true);
@@ -106,7 +107,7 @@ const BlocksField: React.FC<Props> = (props) => {
setTimeout(() => { setTimeout(() => {
scrollToID(`${path}-row-${rowIndex + 1}`); scrollToID(`${path}-row-${rowIndex + 1}`);
}, 0); }, 0);
}, [path, blocks, dispatchFields, operation, id, user, locale, setModified]); }, [blocks, operation, id, user, locale, t, dispatchFields, path, setModified]);
const duplicateRow = useCallback(async (rowIndex: number, blockType: string) => { const duplicateRow = useCallback(async (rowIndex: number, blockType: string) => {
dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path }); dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path });
@@ -223,7 +224,7 @@ const BlocksField: React.FC<Props> = (props) => {
</div> </div>
<header className={`${baseClass}__header`}> <header className={`${baseClass}__header`}>
<div className={`${baseClass}__header-wrap`}> <div className={`${baseClass}__header-wrap`}>
<h3>{label}</h3> <h3>{getTranslation(label || name, i18n)}</h3>
<ul className={`${baseClass}__header-actions`}> <ul className={`${baseClass}__header-actions`}>
<li> <li>
<button <button
@@ -231,7 +232,7 @@ const BlocksField: React.FC<Props> = (props) => {
onClick={() => toggleCollapseAll(true)} onClick={() => toggleCollapseAll(true)}
className={`${baseClass}__header-action`} className={`${baseClass}__header-action`}
> >
Collapse All {t('collapseAll')}
</button> </button>
</li> </li>
<li> <li>
@@ -240,7 +241,7 @@ const BlocksField: React.FC<Props> = (props) => {
onClick={() => toggleCollapseAll(false)} onClick={() => toggleCollapseAll(false)}
className={`${baseClass}__header-action`} className={`${baseClass}__header-action`}
> >
Show All {t('showAll')}
</button> </button>
</li> </li>
</ul> </ul>
@@ -295,7 +296,7 @@ const BlocksField: React.FC<Props> = (props) => {
pillStyle="white" pillStyle="white"
className={`${baseClass}__block-pill ${baseClass}__block-pill-${blockType}`} className={`${baseClass}__block-pill ${baseClass}__block-pill-${blockType}`}
> >
{blockToRender.labels.singular} {getTranslation(blockToRender.labels.singular, i18n)}
</Pill> </Pill>
<SectionTitle <SectionTitle
path={`${path}.${i}.blockName`} path={`${path}.${i}.blockName`}
@@ -360,17 +361,15 @@ const BlocksField: React.FC<Props> = (props) => {
})} })}
{(rows.length < minRows || (required && rows.length === 0)) && ( {(rows.length < minRows || (required && rows.length === 0)) && (
<Banner type="error"> <Banner type="error">
This field requires at least {t('requiresAtLeast', {
{' '} count: minRows,
{`${minRows || 1} ${minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural}`} label: getTranslation(minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural, i18n),
})}
</Banner> </Banner>
)} )}
{(rows.length === 0 && readOnly) && ( {(rows.length === 0 && readOnly) && (
<Banner> <Banner>
This field has no {t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
{' '}
{labels.plural}
.
</Banner> </Banner>
)} )}
{provided.placeholder} {provided.placeholder}
@@ -391,7 +390,7 @@ const BlocksField: React.FC<Props> = (props) => {
iconPosition="left" iconPosition="left"
iconStyle="with-border" iconStyle="with-border"
> >
{`Add ${labels.singular}`} {t('addLabel', { label: getTranslation(labels.singular, i18n) })}
</Button> </Button>
)} )}
render={({ close }) => ( render={({ close }) => (

View File

@@ -1,4 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import useField from '../../useField'; import useField from '../../useField';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import Error from '../../Error'; import Error from '../../Error';
@@ -6,6 +7,7 @@ import { checkbox } from '../../../../../fields/validations';
import Check from '../../../icons/Check'; import Check from '../../../icons/Check';
import FieldDescription from '../../FieldDescription'; import FieldDescription from '../../FieldDescription';
import { Props } from './types'; import { Props } from './types';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -30,6 +32,8 @@ const Checkbox: React.FC<Props> = (props) => {
} = {}, } = {},
} = props; } = props;
const { i18n } = useTranslation();
const path = pathFromProps || name; const path = pathFromProps || name;
const memoizedValidate = useCallback((value, options) => { const memoizedValidate = useCallback((value, options) => {
@@ -87,7 +91,7 @@ const Checkbox: React.FC<Props> = (props) => {
<Check /> <Check />
</span> </span>
<span className={`${baseClass}__label`}> <span className={`${baseClass}__label`}>
{label} {getTranslation(label || name, i18n)}
</span> </span>
</button> </button>
<FieldDescription <FieldDescription

View File

@@ -1,26 +1,28 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import useField from '../../useField'; import useField from '../../useField';
import Label from '../../Label'; import Label from '../../Label';
import Error from '../../Error'; import Error from '../../Error';
import { useFormFields } from '../../Form/context'; import { useFormFields } from '../../Form/context';
import { Field } from '../../Form/types';
import './index.scss'; import './index.scss';
import { Field } from '../../Form/types';
const ConfirmPassword: React.FC = () => { const ConfirmPassword: React.FC = () => {
const password = useFormFields<Field>(([fields]) => fields.password); const password = useFormFields<Field>(([fields]) => fields.password);
const { t } = useTranslation('fields');
const validate = useCallback((value: string) => { const validate = useCallback((value: string) => {
if (!value) { if (!value) {
return 'This field is required'; return t('validation:required');
} }
if (value === password?.value) { if (value === password?.value) {
return true; return true;
} }
return 'Passwords do not match.'; return t('passwordsDoNotMatch');
}, [password]); }, [password, t]);
const { const {
value, value,
@@ -47,7 +49,7 @@ const ConfirmPassword: React.FC = () => {
/> />
<Label <Label
htmlFor="field-confirm-password" htmlFor="field-confirm-password"
label="Confirm Password" label={t('authentication:confirmPassword')}
required required
/> />
<input <input

View File

@@ -1,5 +1,6 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import DatePicker from '../../../elements/DatePicker'; import DatePicker from '../../../elements/DatePicker';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import useField from '../../useField'; import useField from '../../useField';
@@ -8,6 +9,7 @@ import Error from '../../Error';
import FieldDescription from '../../FieldDescription'; import FieldDescription from '../../FieldDescription';
import { date as dateValidation } from '../../../../../fields/validations'; import { date as dateValidation } from '../../../../../fields/validations';
import { Props } from './types'; import { Props } from './types';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -32,6 +34,8 @@ const DateTime: React.FC<Props> = (props) => {
} = {}, } = {},
} = props; } = props;
const { i18n } = useTranslation();
const path = pathFromProps || name; const path = pathFromProps || name;
const memoizedValidate = useCallback((value, options) => { const memoizedValidate = useCallback((value, options) => {
@@ -82,7 +86,7 @@ const DateTime: React.FC<Props> = (props) => {
> >
<DatePicker <DatePicker
{...date} {...date}
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
readOnly={readOnly} readOnly={readOnly}
onChange={readOnly ? undefined : setValue} onChange={readOnly ? undefined : setValue}
value={value as Date} value={value as Date}

View File

@@ -1,4 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import useField from '../../useField'; import useField from '../../useField';
import Label from '../../Label'; import Label from '../../Label';
@@ -6,6 +7,7 @@ import Error from '../../Error';
import FieldDescription from '../../FieldDescription'; import FieldDescription from '../../FieldDescription';
import { email } from '../../../../../fields/validations'; import { email } from '../../../../../fields/validations';
import { Props } from './types'; import { Props } from './types';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -28,6 +30,8 @@ const Email: React.FC<Props> = (props) => {
label, label,
} = props; } = props;
const { i18n } = useTranslation();
const path = pathFromProps || name; const path = pathFromProps || name;
const memoizedValidate = useCallback((value, options) => { const memoizedValidate = useCallback((value, options) => {
@@ -77,7 +81,7 @@ const Email: React.FC<Props> = (props) => {
value={value as string || ''} value={value as string || ''}
onChange={setValue} onChange={setValue}
disabled={Boolean(readOnly)} disabled={Boolean(readOnly)}
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
type="email" type="email"
name={path} name={path}
autoComplete={autoComplete} autoComplete={autoComplete}

View File

@@ -1,14 +1,16 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import RenderFields from '../../RenderFields'; import RenderFields from '../../RenderFields';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import FieldDescription from '../../FieldDescription'; import FieldDescription from '../../FieldDescription';
import { Props } from './types'; import { Props } from './types';
import { fieldAffectsData } from '../../../../../fields/config/types'; import { fieldAffectsData } from '../../../../../fields/config/types';
import { useCollapsible } from '../../../elements/Collapsible/provider'; import { useCollapsible } from '../../../elements/Collapsible/provider';
import './index.scss';
import { GroupProvider, useGroup } from './provider'; import { GroupProvider, useGroup } from './provider';
import { useTabs } from '../Tabs/provider'; import { useTabs } from '../Tabs/provider';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss';
const baseClass = 'group-field'; const baseClass = 'group-field';
@@ -34,6 +36,7 @@ const Group: React.FC<Props> = (props) => {
const isWithinCollapsible = useCollapsible(); const isWithinCollapsible = useCollapsible();
const isWithinGroup = useGroup(); const isWithinGroup = useGroup();
const isWithinTab = useTabs(); const isWithinTab = useTabs();
const { i18n } = useTranslation();
const path = pathFromProps || name; const path = pathFromProps || name;
@@ -59,9 +62,10 @@ const Group: React.FC<Props> = (props) => {
{(label || description) && ( {(label || description) && (
<header className={`${baseClass}__header`}> <header className={`${baseClass}__header`}>
{label && ( {label && (
<h3 className={`${baseClass}__title`}>{label}</h3> <h3 className={`${baseClass}__title`}>{getTranslation(label, i18n)}</h3>
)} )}
<FieldDescription <FieldDescription
className={`field-description-${path.replace(/\./gi, '__')}`}
value={null} value={null}
description={description} description={description}
/> />

View File

@@ -1,4 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import useField from '../../useField'; import useField from '../../useField';
import Label from '../../Label'; import Label from '../../Label';
import Error from '../../Error'; import Error from '../../Error';
@@ -6,6 +7,7 @@ import FieldDescription from '../../FieldDescription';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import { number } from '../../../../../fields/validations'; import { number } from '../../../../../fields/validations';
import { Props } from './types'; import { Props } from './types';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -30,6 +32,8 @@ const NumberField: React.FC<Props> = (props) => {
} = {}, } = {},
} = props; } = props;
const { i18n } = useTranslation();
const path = pathFromProps || name; const path = pathFromProps || name;
const memoizedValidate = useCallback((value, options) => { const memoizedValidate = useCallback((value, options) => {
@@ -87,7 +91,7 @@ const NumberField: React.FC<Props> = (props) => {
value={typeof value === 'number' ? value : ''} value={typeof value === 'number' ? value : ''}
onChange={handleChange} onChange={handleChange}
disabled={readOnly} disabled={readOnly}
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
type="number" type="number"
name={path} name={path}
step={step} step={step}

View File

@@ -1,4 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import useField from '../../useField'; import useField from '../../useField';
import Label from '../../Label'; import Label from '../../Label';
import Error from '../../Error'; import Error from '../../Error';
@@ -6,6 +7,7 @@ import FieldDescription from '../../FieldDescription';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import { point } from '../../../../../fields/validations'; import { point } from '../../../../../fields/validations';
import { Props } from './types'; import { Props } from './types';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -32,6 +34,8 @@ const PointField: React.FC<Props> = (props) => {
const path = pathFromProps || name; const path = pathFromProps || name;
const { t, i18n } = useTranslation('fields');
const memoizedValidate = useCallback((value, options) => { const memoizedValidate = useCallback((value, options) => {
return validate(value, { ...options, required }); return validate(value, { ...options, required });
}, [validate, required]); }, [validate, required]);
@@ -81,7 +85,7 @@ const PointField: React.FC<Props> = (props) => {
<li> <li>
<Label <Label
htmlFor={`field-longitude-${path.replace(/\./gi, '__')}`} htmlFor={`field-longitude-${path.replace(/\./gi, '__')}`}
label={`${label} - Longitude`} label={`${getTranslation(label || name, i18n)} - ${t('longitude')}`}
required={required} required={required}
/> />
<input <input
@@ -89,7 +93,7 @@ const PointField: React.FC<Props> = (props) => {
value={(value && typeof value[0] === 'number') ? value[0] : ''} value={(value && typeof value[0] === 'number') ? value[0] : ''}
onChange={(e) => handleChange(e, 0)} onChange={(e) => handleChange(e, 0)}
disabled={readOnly} disabled={readOnly}
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
type="number" type="number"
name={`${path}.longitude`} name={`${path}.longitude`}
step={step} step={step}
@@ -98,7 +102,7 @@ const PointField: React.FC<Props> = (props) => {
<li> <li>
<Label <Label
htmlFor={`field-latitude-${path.replace(/\./gi, '__')}`} htmlFor={`field-latitude-${path.replace(/\./gi, '__')}`}
label={`${label} - Latitude`} label={`${getTranslation(label || name, i18n)} - ${t('latitude')}`}
required={required} required={required}
/> />
<input <input
@@ -106,7 +110,7 @@ const PointField: React.FC<Props> = (props) => {
value={(value && typeof value[1] === 'number') ? value[1] : ''} value={(value && typeof value[1] === 'number') ? value[1] : ''}
onChange={(e) => handleChange(e, 1)} onChange={(e) => handleChange(e, 1)}
disabled={readOnly} disabled={readOnly}
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
type="number" type="number"
name={`${path}.latitude`} name={`${path}.latitude`}
step={step} step={step}

View File

@@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { Props } from './types'; import { Props } from './types';
import { getTranslation } from '../../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -7,6 +9,7 @@ const baseClass = 'radio-input';
const RadioInput: React.FC<Props> = (props) => { const RadioInput: React.FC<Props> = (props) => {
const { isSelected, option, onChange, path } = props; const { isSelected, option, onChange, path } = props;
const { i18n } = useTranslation();
const classes = [ const classes = [
baseClass, baseClass,
@@ -27,7 +30,7 @@ const RadioInput: React.FC<Props> = (props) => {
onChange={() => (typeof onChange === 'function' ? onChange(option.value) : null)} onChange={() => (typeof onChange === 'function' ? onChange(option.value) : null)}
/> />
<span className={`${baseClass}__styled-radio`} /> <span className={`${baseClass}__styled-radio`} />
<span className={`${baseClass}__label`}>{option.label}</span> <span className={`${baseClass}__label`}>{getTranslation(option.label, i18n)}</span>
</div> </div>
</label> </label>
); );

View File

@@ -3,7 +3,7 @@ import { OnChange } from '../types';
export type Props = { export type Props = {
isSelected: boolean isSelected: boolean
option: { option: {
label: string label: Record<string, string> | string
value: string value: string
} }
onChange: OnChange onChange: OnChange

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, useModal } from '@faceless-ui/modal'; import { Modal, useModal } from '@faceless-ui/modal';
import { useWindowInfo } from '@faceless-ui/window-info'; import { useWindowInfo } from '@faceless-ui/window-info';
import { useTranslation } from 'react-i18next';
import Button from '../../../../../elements/Button'; import Button from '../../../../../elements/Button';
import { Props } from './types'; import { Props } from './types';
import { useAuth } from '../../../../../utilities/Auth'; import { useAuth } from '../../../../../utilities/Auth';
@@ -12,6 +13,7 @@ import X from '../../../../../icons/X';
import { Fields } from '../../../../Form/types'; import { Fields } from '../../../../Form/types';
import buildStateFromSchema from '../../../../Form/buildStateFromSchema'; import buildStateFromSchema from '../../../../Form/buildStateFromSchema';
import { EditDepthContext, useEditDepth } from '../../../../../utilities/EditDepth'; import { EditDepthContext, useEditDepth } from '../../../../../utilities/EditDepth';
import { getTranslation } from '../../../../../../../utilities/getTranslation';
import { DocumentInfoProvider } from '../../../../../utilities/DocumentInfo'; import { DocumentInfoProvider } from '../../../../../utilities/DocumentInfo';
import './index.scss'; import './index.scss';
@@ -27,17 +29,18 @@ export const AddNewRelationModal: React.FC<Props> = ({ modalCollection, onSave,
const [initialState, setInitialState] = useState<Fields>(); const [initialState, setInitialState] = useState<Fields>();
const [isAnimated, setIsAnimated] = useState(false); const [isAnimated, setIsAnimated] = useState(false);
const editDepth = useEditDepth(); const editDepth = useEditDepth();
const { t, i18n } = useTranslation('fields');
const modalAction = `${serverURL}${api}/${modalCollection.slug}?locale=${locale}&depth=0&fallback-locale=null`; const modalAction = `${serverURL}${api}/${modalCollection.slug}?locale=${locale}&depth=0&fallback-locale=null`;
useEffect(() => { useEffect(() => {
const buildState = async () => { const buildState = async () => {
const state = await buildStateFromSchema({ fieldSchema: modalCollection.fields, data: {}, user, operation: 'create', locale }); const state = await buildStateFromSchema({ fieldSchema: modalCollection.fields, data: {}, user, operation: 'create', locale, t });
setInitialState(state); setInitialState(state);
}; };
buildState(); buildState();
}, [modalCollection, locale, user]); }, [modalCollection, locale, user, t]);
useEffect(() => { useEffect(() => {
setIsAnimated(true); setIsAnimated(true);
@@ -87,9 +90,7 @@ export const AddNewRelationModal: React.FC<Props> = ({ modalCollection, onSave,
customHeader: ( customHeader: (
<div className={`${baseClass}__header`}> <div className={`${baseClass}__header`}>
<h2> <h2>
Add new {t('addNewLabel', { label: getTranslation(modalCollection.labels.singular, i18n) })}
{' '}
{modalCollection.labels.singular}
</h2> </h2>
<Button <Button
buttonStyle="none" buttonStyle="none"

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useModal } from '@faceless-ui/modal'; import { useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import Button from '../../../../elements/Button'; import Button from '../../../../elements/Button';
import { Props } from './types'; import { Props } from './types';
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types'; import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
@@ -9,6 +10,7 @@ import { useAuth } from '../../../../utilities/Auth';
import { AddNewRelationModal } from './Modal'; import { AddNewRelationModal } from './Modal';
import { useEditDepth } from '../../../../utilities/EditDepth'; import { useEditDepth } from '../../../../utilities/EditDepth';
import Plus from '../../../../icons/Plus'; import Plus from '../../../../icons/Plus';
import { getTranslation } from '../../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -22,6 +24,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>(); const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>();
const [popupOpen, setPopupOpen] = useState(false); const [popupOpen, setPopupOpen] = useState(false);
const editDepth = useEditDepth(); const editDepth = useEditDepth();
const { t, i18n } = useTranslation('fields');
const modalSlug = `${path}-add-modal-depth-${editDepth}`; const modalSlug = `${path}-add-modal-depth-${editDepth}`;
@@ -44,6 +47,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
json.doc, json.doc,
], ],
sort: true, sort: true,
i18n,
}); });
if (hasMany) { if (hasMany) {
@@ -54,7 +58,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
setModalCollection(undefined); setModalCollection(undefined);
toggleModal(modalSlug); toggleModal(modalSlug);
}, [relationTo, modalCollection, hasMany, toggleModal, modalSlug, setValue, value, dispatchOptions]); }, [relationTo, modalCollection, dispatchOptions, i18n, hasMany, toggleModal, modalSlug, setValue, value]);
const onPopopToggle = useCallback((state) => { const onPopopToggle = useCallback((state) => {
setPopupOpen(state); setPopupOpen(state);
@@ -86,7 +90,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
className={`${baseClass}__add-button`} className={`${baseClass}__add-button`}
onClick={() => openModal(relatedCollections[0])} onClick={() => openModal(relatedCollections[0])}
buttonStyle="none" buttonStyle="none"
tooltip={`Add new ${relatedCollections[0].labels.singular}`} tooltip={t('addNewLabel', { label: relatedCollections[0].labels.singular })}
> >
<Plus /> <Plus />
</Button> </Button>
@@ -100,7 +104,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
<Button <Button
className={`${baseClass}__add-button`} className={`${baseClass}__add-button`}
buttonStyle="none" buttonStyle="none"
tooltip={popupOpen ? undefined : 'Add new'} tooltip={popupOpen ? undefined : t('addNew')}
> >
<Plus /> <Plus />
</Button> </Button>
@@ -116,7 +120,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
type="button" type="button"
onClick={() => { closePopup(); openModal(relatedCollection); }} onClick={() => { closePopup(); openModal(relatedCollection); }}
> >
{relatedCollection.labels.singular} {getTranslation(relatedCollection.labels.singular, i18n)}
</button> </button>
</li> </li>
); );

View File

@@ -3,6 +3,7 @@ import React, {
} from 'react'; } from 'react';
import equal from 'deep-equal'; import equal from 'deep-equal';
import qs from 'qs'; import qs from 'qs';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../utilities/Config'; import { useConfig } from '../../../utilities/Config';
import { useAuth } from '../../../utilities/Auth'; import { useAuth } from '../../../utilities/Auth';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
@@ -62,12 +63,13 @@ const Relationship: React.FC<Props> = (props) => {
collections, collections,
} = useConfig(); } = useConfig();
const { t, i18n } = useTranslation('fields');
const { id } = useDocumentInfo(); const { id } = useDocumentInfo();
const { user, permissions } = useAuth(); const { user, permissions } = useAuth();
const [fields] = useAllFormFields(); const [fields] = useAllFormFields();
const formProcessing = useFormProcessing(); const formProcessing = useFormProcessing();
const hasMultipleRelations = Array.isArray(relationTo); const hasMultipleRelations = Array.isArray(relationTo);
const [options, dispatchOptions] = useReducer(optionsReducer, required || hasMany ? [] : [{ value: null, label: 'None' }]); const [options, dispatchOptions] = useReducer(optionsReducer, required || hasMany ? [] : [{ value: null, label: t('general:none') }]);
const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1); const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1);
const [lastLoadedPage, setLastLoadedPage] = useState(1); const [lastLoadedPage, setLastLoadedPage] = useState(1);
const [errorLoading, setErrorLoading] = useState(''); const [errorLoading, setErrorLoading] = useState('');
@@ -159,13 +161,18 @@ const Relationship: React.FC<Props> = (props) => {
query.where.and.push(optionFilters[relation]); query.where.and.push(optionFilters[relation]);
} }
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, { credentials: 'include' }); const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
});
if (response.ok) { if (response.ok) {
const data: PaginatedDocs<unknown> = await response.json(); const data: PaginatedDocs<unknown> = await response.json();
if (data.docs.length > 0) { if (data.docs.length > 0) {
resultsFetched += data.docs.length; resultsFetched += data.docs.length;
dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort }); dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort, i18n });
setLastLoadedPage(data.page); setLastLoadedPage(data.page);
if (!data.nextPage) { if (!data.nextPage) {
@@ -181,9 +188,9 @@ const Relationship: React.FC<Props> = (props) => {
} else if (response.status === 403) { } else if (response.status === 403) {
setLastFullyLoadedRelation(relations.indexOf(relation)); setLastFullyLoadedRelation(relations.indexOf(relation));
lastLoadedPageToUse = 1; lastLoadedPageToUse = 1;
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort, ids: relationMap[relation] }); dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort, ids: relationMap[relation], i18n });
} else { } else {
setErrorLoading('An error has occurred.'); setErrorLoading(t('error:unspecific'));
} }
} }
}, Promise.resolve()); }, Promise.resolve());
@@ -198,6 +205,8 @@ const Relationship: React.FC<Props> = (props) => {
serverURL, serverURL,
api, api,
hasMultipleRelations, hasMultipleRelations,
t,
i18n,
]); ]);
const findOptionsByValue = useCallback((): Option | Option[] => { const findOptionsByValue = useCallback((): Option | Option[] => {
@@ -295,13 +304,18 @@ const Relationship: React.FC<Props> = (props) => {
}; };
if (!errorLoading) { if (!errorLoading) {
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, { credentials: 'include' }); const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
});
const collection = collections.find((coll) => coll.slug === relation); const collection = collections.find((coll) => coll.slug === relation);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort: true, ids }); dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort: true, ids, i18n });
} else if (response.status === 403) { } else if (response.status === 403) {
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort: true, ids }); dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort: true, ids, i18n });
} }
} }
} }
@@ -309,7 +323,7 @@ const Relationship: React.FC<Props> = (props) => {
setHasLoadedValueOptions(true); setHasLoadedValueOptions(true);
} }
}, [hasMany, hasMultipleRelations, relationTo, initialValue, hasLoadedValueOptions, errorLoading, collections, api, serverURL]); }, [hasMany, hasMultipleRelations, relationTo, initialValue, hasLoadedValueOptions, errorLoading, collections, api, serverURL, i18n]);
useEffect(() => { useEffect(() => {
if (!filterOptions) return; if (!filterOptions) return;

View File

@@ -1,4 +1,5 @@
import { Option, Action } from './types'; import { Option, Action } from './types';
import { getTranslation } from '../../../../../utilities/getTranslation';
const reduceToIDs = (options) => options.reduce((ids, option) => { const reduceToIDs = (options) => options.reduce((ids, option) => {
if (option.options) { if (option.options) {
@@ -29,7 +30,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
} }
case 'ADD': { case 'ADD': {
const { hasMultipleRelations, collection, docs, sort, ids = [] } = action; const { hasMultipleRelations, collection, docs, sort, ids = [], i18n } = action;
const relation = collection.slug; const relation = collection.slug;
const labelKey = collection.admin.useAsTitle || 'id'; const labelKey = collection.admin.useAsTitle || 'id';
@@ -45,7 +46,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
return [ return [
...docOptions, ...docOptions,
{ {
label: doc[labelKey] || `Untitled - ID: ${doc.id}`, label: doc[labelKey] || `${i18n.t('general:untitled')} - ID: ${doc.id}`,
value: doc.id, value: doc.id,
}, },
]; ];
@@ -57,7 +58,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
ids.forEach((id) => { ids.forEach((id) => {
if (!loadedIDs.includes(id)) { if (!loadedIDs.includes(id)) {
options.push({ options.push({
label: labelKey === 'id' ? id : `Untitled - ID: ${id}`, label: labelKey === 'id' ? id : `${i18n.t('general:untitled')} - ID: ${id}`,
value: id, value: id,
}); });
} }
@@ -76,7 +77,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
return [ return [
...docSubOptions, ...docSubOptions,
{ {
label: doc[labelKey] || `Untitled - ID: ${doc.id}`, label: doc[labelKey] || `${i18n.t('general:untitled')} - ID: ${doc.id}`,
relationTo: relation, relationTo: relation,
value: doc.id, value: doc.id,
}, },
@@ -89,7 +90,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
ids.forEach((id) => { ids.forEach((id) => {
if (!loadedIDs.includes(id)) { if (!loadedIDs.includes(id)) {
newSubOptions.push({ newSubOptions.push({
label: labelKey === 'id' ? id : `Untitled - ID: ${id}`, label: labelKey === 'id' ? id : `${i18n.t('general:untitled')} - ID: ${id}`,
value: id, value: id,
}); });
} }
@@ -104,7 +105,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
optionsToAddTo.options = sort ? sortOptions(subOptions) : subOptions; optionsToAddTo.options = sort ? sortOptions(subOptions) : subOptions;
} else { } else {
newOptions.push({ newOptions.push({
label: collection.labels.plural, label: getTranslation(collection.labels.plural, i18n),
options: sort ? sortOptions(newSubOptions) : newSubOptions, options: sort ? sortOptions(newSubOptions) : newSubOptions,
value: undefined, value: undefined,
}); });

View File

@@ -1,3 +1,4 @@
import i18n from 'i18next';
import { SanitizedCollectionConfig } from '../../../../../collections/config/types'; import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { RelationshipField } from '../../../../../fields/config/types'; import { RelationshipField } from '../../../../../fields/config/types';
@@ -23,6 +24,7 @@ type ADD = {
collection: SanitizedCollectionConfig collection: SanitizedCollectionConfig
sort?: boolean sort?: boolean
ids?: unknown[] ids?: unknown[]
i18n: typeof i18n
} }
export type Action = CLEAR | ADD export type Action = CLEAR | ADD

View File

@@ -3,6 +3,7 @@ import isHotkey from 'is-hotkey';
import { createEditor, Transforms, Node, Element as SlateElement, Text, BaseEditor } from 'slate'; import { createEditor, Transforms, Node, Element as SlateElement, Text, BaseEditor } from 'slate';
import { ReactEditor, Editable, withReact, Slate } from 'slate-react'; import { ReactEditor, Editable, withReact, Slate } from 'slate-react';
import { HistoryEditor, withHistory } from 'slate-history'; import { HistoryEditor, withHistory } from 'slate-history';
import { useTranslation } from 'react-i18next';
import { richText } from '../../../../../fields/validations'; import { richText } from '../../../../../fields/validations';
import useField from '../../useField'; import useField from '../../useField';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
@@ -21,6 +22,7 @@ import { RichTextElement, RichTextLeaf } from '../../../../../fields/config/type
import listTypes from './elements/listTypes'; import listTypes from './elements/listTypes';
import mergeCustomFunctions from './mergeCustomFunctions'; import mergeCustomFunctions from './mergeCustomFunctions';
import withEnterBreakOut from './plugins/withEnterBreakOut'; import withEnterBreakOut from './plugins/withEnterBreakOut';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -65,6 +67,7 @@ const RichText: React.FC<Props> = (props) => {
const path = pathFromProps || name; const path = pathFromProps || name;
const { i18n } = useTranslation();
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const [enabledElements, setEnabledElements] = useState({}); const [enabledElements, setEnabledElements] = useState({});
const [enabledLeaves, setEnabledLeaves] = useState({}); const [enabledLeaves, setEnabledLeaves] = useState({});
@@ -308,7 +311,7 @@ const RichText: React.FC<Props> = (props) => {
className={`${baseClass}__input`} className={`${baseClass}__input`}
renderElement={renderElement} renderElement={renderElement}
renderLeaf={renderLeaf} renderLeaf={renderLeaf}
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
spellCheck spellCheck
readOnly={readOnly} readOnly={readOnly}
onKeyDown={(event) => { onKeyDown={(event) => {

View File

@@ -2,6 +2,7 @@ import React, { Fragment, useState } from 'react';
import { ReactEditor, useSlate } from 'slate-react'; import { ReactEditor, useSlate } from 'slate-react';
import { Transforms, Editor, Range } from 'slate'; import { Transforms, Editor, Range } from 'slate';
import { useModal } from '@faceless-ui/modal'; import { useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import ElementButton from '../Button'; import ElementButton from '../Button';
import { unwrapLink } from './utilities'; import { unwrapLink } from './utilities';
import LinkIcon from '../../../../../icons/Link'; import LinkIcon from '../../../../../icons/Link';
@@ -22,6 +23,7 @@ export const LinkButton = ({ fieldProps }) => {
const modalSlug = `${baseModalSlug}-${fieldProps.path}`; const modalSlug = `${baseModalSlug}-${fieldProps.path}`;
const { t } = useTranslation();
const config = useConfig(); const config = useConfig();
const editor = useSlate(); const editor = useSlate();
const { user } = useAuth(); const { user } = useAuth();
@@ -71,7 +73,7 @@ export const LinkButton = ({ fieldProps }) => {
text: editor.selection ? Editor.string(editor, editor.selection) : '', text: editor.selection ? Editor.string(editor, editor.selection) : '',
}; };
const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'create', locale }); const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'create', locale, t });
setInitialState(state); setInitialState(state);
} }
} }

View File

@@ -1,7 +1,8 @@
import React, { Fragment, useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { ReactEditor, useSlate } from 'slate-react'; import { ReactEditor, useSlate } from 'slate-react';
import { Transforms, Node, Editor } from 'slate'; import { Transforms, Node, Editor } from 'slate';
import { useModal } from '@faceless-ui/modal'; import { useModal } from '@faceless-ui/modal';
import { Trans, useTranslation } from 'react-i18next';
import { unwrapLink } from './utilities'; import { unwrapLink } from './utilities';
import Popup from '../../../../../elements/Popup'; import Popup from '../../../../../elements/Popup';
import { EditModal } from './Modal'; import { EditModal } from './Modal';
@@ -30,6 +31,7 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
const config = useConfig(); const config = useConfig();
const { user } = useAuth(); const { user } = useAuth();
const locale = useLocale(); const locale = useLocale();
const { t } = useTranslation('fields');
const { openModal, toggleModal } = useModal(); const { openModal, toggleModal } = useModal();
const [renderModal, setRenderModal] = useState(false); const [renderModal, setRenderModal] = useState(false);
const [renderPopup, setRenderPopup] = useState(false); const [renderPopup, setRenderPopup] = useState(false);
@@ -77,12 +79,12 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
fields: deepCopyObject(element.fields), fields: deepCopyObject(element.fields),
}; };
const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'update', locale }); const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'update', locale, t });
setInitialState(state); setInitialState(state);
}; };
awaitInitialState(); awaitInitialState();
}, [renderModal, element, fieldSchema, user, locale]); }, [renderModal, element, fieldSchema, user, locale, t]);
return ( return (
<span <span
@@ -146,17 +148,20 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
render={() => ( render={() => (
<div className={`${baseClass}__popup`}> <div className={`${baseClass}__popup`}>
{element.linkType === 'internal' && element.doc?.relationTo && element.doc?.value && ( {element.linkType === 'internal' && element.doc?.relationTo && element.doc?.value && (
<Fragment> <Trans
Linked to&nbsp; i18nKey="linkedTo"
values={{ label: config.collections.find(({ slug }) => slug === element.doc.relationTo)?.labels?.singular }}
>
linkedTo
<a <a
className={`${baseClass}__link-label`} className={`${baseClass}__link-label`}
href={`${config.routes.admin}/collections/${element.doc.relationTo}/${element.doc.value}`} href={`${config.routes.admin}/collections/${element.doc.relationTo}/${element.doc.value}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
{config.collections.find(({ slug }) => slug === element.doc.relationTo)?.labels?.singular} label
</a> </a>
</Fragment> </Trans>
)} )}
{(element.linkType === 'custom' || !element.linkType) && ( {(element.linkType === 'custom' || !element.linkType) && (
<a <a
@@ -179,7 +184,7 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
openModal(modalSlug); openModal(modalSlug);
setRenderModal(true); setRenderModal(true);
}} }}
tooltip="Edit" tooltip={t('general:edit')}
/> />
<Button <Button
className={`${baseClass}__link-close`} className={`${baseClass}__link-close`}
@@ -190,7 +195,7 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
e.preventDefault(); e.preventDefault();
unwrapLink(editor); unwrapLink(editor);
}} }}
tooltip="Remove" tooltip={t('general:remove')}
/> />
</div> </div>
)} )}

View File

@@ -1,4 +1,5 @@
import React, { Fragment, useState, useEffect } from 'react'; import React, { Fragment, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../../../../utilities/Config'; import { useConfig } from '../../../../../../../utilities/Config';
import { useAuth } from '../../../../../../../utilities/Auth'; import { useAuth } from '../../../../../../../utilities/Auth';
import { useFormFields } from '../../../../../../Form/context'; import { useFormFields } from '../../../../../../Form/context';
@@ -22,6 +23,7 @@ const createOptions = (collections, permissions) => collections.reduce((options,
const RelationshipFields = () => { const RelationshipFields = () => {
const { collections } = useConfig(); const { collections } = useConfig();
const { permissions } = useAuth(); const { permissions } = useAuth();
const { t } = useTranslation('fields');
const [options, setOptions] = useState(() => createOptions(collections, permissions)); const [options, setOptions] = useState(() => createOptions(collections, permissions));
const relationTo = useFormFields<string>(([fields]) => fields.relationTo?.value as string); const relationTo = useFormFields<string>(([fields]) => fields.relationTo?.value as string);
@@ -34,13 +36,13 @@ const RelationshipFields = () => {
<Fragment> <Fragment>
<Select <Select
required required
label="Relation To" label={t('relationTo')}
name="relationTo" name="relationTo"
options={options} options={options}
/> />
{relationTo && ( {relationTo && (
<Relationship <Relationship
label="Related Document" label={t('relatedDocument')}
name="value" name="value"
relationTo={relationTo} relationTo={relationTo}
required required

View File

@@ -1,6 +1,7 @@
import React, { Fragment, useCallback, useEffect, useState } from 'react'; import React, { Fragment, useCallback, useEffect, useState } from 'react';
import { Modal, useModal } from '@faceless-ui/modal'; import { Modal, useModal } from '@faceless-ui/modal';
import { ReactEditor, useSlate } from 'slate-react'; import { ReactEditor, useSlate } from 'slate-react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../../../utilities/Config'; import { useConfig } from '../../../../../../utilities/Config';
import ElementButton from '../../Button'; import ElementButton from '../../Button';
import RelationshipIcon from '../../../../../../icons/Relationship'; import RelationshipIcon from '../../../../../../icons/Relationship';
@@ -42,20 +43,25 @@ const RelationshipButton: React.FC<{ path: string }> = ({ path }) => {
const { serverURL, routes: { api }, collections } = useConfig(); const { serverURL, routes: { api }, collections } = useConfig();
const [renderModal, setRenderModal] = useState(false); const [renderModal, setRenderModal] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { t, i18n } = useTranslation('fields');
const [hasEnabledCollections] = useState(() => collections.find(({ admin: { enableRichTextRelationship } }) => enableRichTextRelationship)); const [hasEnabledCollections] = useState(() => collections.find(({ admin: { enableRichTextRelationship } }) => enableRichTextRelationship));
const modalSlug = `${path}-add-relationship`; const modalSlug = `${path}-add-relationship`;
const handleAddRelationship = useCallback(async (_, { relationTo, value }) => { const handleAddRelationship = useCallback(async (_, { relationTo, value }) => {
setLoading(true); setLoading(true);
const res = await requests.get(`${serverURL}${api}/${relationTo}/${value}?depth=0`); const res = await requests.get(`${serverURL}${api}/${relationTo}/${value}?depth=0`, {
headers: {
'Accept-Language': i18n.language,
},
});
const json = await res.json(); const json = await res.json();
insertRelationship(editor, { value: { id: json.id }, relationTo }); insertRelationship(editor, { value: { id: json.id }, relationTo });
toggleModal(modalSlug); toggleModal(modalSlug);
setRenderModal(false); setRenderModal(false);
setLoading(false); setLoading(false);
}, [editor, toggleModal, modalSlug, api, serverURL]); }, [i18n.language, editor, toggleModal, modalSlug, api, serverURL]);
useEffect(() => { useEffect(() => {
if (renderModal) { if (renderModal) {
@@ -81,7 +87,7 @@ const RelationshipButton: React.FC<{ path: string }> = ({ path }) => {
> >
<MinimalTemplate className={`${baseClass}__modal-template`}> <MinimalTemplate className={`${baseClass}__modal-template`}>
<header className={`${baseClass}__header`}> <header className={`${baseClass}__header`}>
<h3>Add Relationship</h3> <h3>{t('addRelationship')}</h3>
<Button <Button
buttonStyle="none" buttonStyle="none"
onClick={() => { onClick={() => {
@@ -99,7 +105,7 @@ const RelationshipButton: React.FC<{ path: string }> = ({ path }) => {
> >
<Fields /> <Fields />
<Submit> <Submit>
Add relationship {t('addRelationship')}
</Submit> </Submit>
</Form> </Form>
</MinimalTemplate> </MinimalTemplate>

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useFocused, useSelected } from 'slate-react'; import { useFocused, useSelected } from 'slate-react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../../../utilities/Config'; import { useConfig } from '../../../../../../utilities/Config';
import RelationshipIcon from '../../../../../../icons/Relationship'; import RelationshipIcon from '../../../../../../icons/Relationship';
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI'; import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
@@ -19,6 +20,7 @@ const Element = (props) => {
const [relatedCollection] = useState(() => collections.find((coll) => coll.slug === relationTo)); const [relatedCollection] = useState(() => collections.find((coll) => coll.slug === relationTo));
const selected = useSelected(); const selected = useSelected();
const focused = useFocused(); const focused = useFocused();
const { t } = useTranslation('fields');
const [{ data }] = usePayloadAPI( const [{ data }] = usePayloadAPI(
`${serverURL}${api}/${relatedCollection.slug}/${value?.id}`, `${serverURL}${api}/${relatedCollection.slug}/${value?.id}`,
@@ -37,9 +39,7 @@ const Element = (props) => {
<RelationshipIcon /> <RelationshipIcon />
<div className={`${baseClass}__wrap`}> <div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__label`}> <div className={`${baseClass}__label`}>
{relatedCollection.labels.singular} {t('labelRelationship', { label: relatedCollection.labels.singular })}
{' '}
Relationship
</div> </div>
<h5>{data[relatedCollection?.admin?.useAsTitle || 'id']}</h5> <h5>{data[relatedCollection?.admin?.useAsTitle || 'id']}</h5>
</div> </div>

View File

@@ -1,7 +1,7 @@
import React, { Fragment, useEffect, useState } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import { Modal, useModal } from '@faceless-ui/modal'; import { Modal, useModal } from '@faceless-ui/modal';
import { Transforms } from 'slate';
import { ReactEditor, useSlate } from 'slate-react'; import { ReactEditor, useSlate } from 'slate-react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../../../utilities/Config'; import { useConfig } from '../../../../../../utilities/Config';
import ElementButton from '../../Button'; import ElementButton from '../../Button';
import UploadIcon from '../../../../../../icons/Upload'; import UploadIcon from '../../../../../../icons/Upload';
@@ -17,6 +17,7 @@ import Button from '../../../../../../elements/Button';
import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types'; import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types';
import PerPage from '../../../../../../elements/PerPage'; import PerPage from '../../../../../../elements/PerPage';
import { injectVoidElement } from '../../injectVoid'; import { injectVoidElement } from '../../injectVoid';
import { getTranslation } from '../../../../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
import '../addSwapModals.scss'; import '../addSwapModals.scss';
@@ -42,6 +43,7 @@ const insertUpload = (editor, { value, relationTo }) => {
}; };
const UploadButton: React.FC<{ path: string }> = ({ path }) => { const UploadButton: React.FC<{ path: string }> = ({ path }) => {
const { t, i18n } = useTranslation('upload');
const { toggleModal, isModalOpen } = useModal(); const { toggleModal, isModalOpen } = useModal();
const editor = useSlate(); const editor = useSlate();
const { serverURL, routes: { api }, collections } = useConfig(); const { serverURL, routes: { api }, collections } = useConfig();
@@ -50,14 +52,13 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
const [modalCollectionOption, setModalCollectionOption] = useState<{ label: string, value: string }>(() => { const [modalCollectionOption, setModalCollectionOption] = useState<{ label: string, value: string }>(() => {
const firstAvailableCollection = collections.find(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)); const firstAvailableCollection = collections.find(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship));
if (firstAvailableCollection) { if (firstAvailableCollection) {
return { label: firstAvailableCollection.labels.singular, value: firstAvailableCollection.slug }; return { label: getTranslation(firstAvailableCollection.labels.singular, i18n), value: firstAvailableCollection.slug };
} }
return undefined; return undefined;
}); });
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>(() => collections.find(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship))); const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>(() => collections.find(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
const [fields, setFields] = useState(() => (modalCollection ? formatFields(modalCollection, t) : undefined));
const [fields, setFields] = useState(() => (modalCollection ? formatFields(modalCollection) : undefined));
const [limit, setLimit] = useState<number>(); const [limit, setLimit] = useState<number>();
const [sort, setSort] = useState(null); const [sort, setSort] = useState(null);
const [where, setWhere] = useState(null); const [where, setWhere] = useState(null);
@@ -73,9 +74,9 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
useEffect(() => { useEffect(() => {
if (modalCollection) { if (modalCollection) {
setFields(formatFields(modalCollection)); setFields(formatFields(modalCollection, t));
} }
}, [modalCollection]); }, [modalCollection, t]);
useEffect(() => { useEffect(() => {
if (renderModal) { if (renderModal) {
@@ -127,9 +128,7 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
<MinimalTemplate width="wide"> <MinimalTemplate width="wide">
<header className={`${baseModalClass}__header`}> <header className={`${baseModalClass}__header`}>
<h1> <h1>
Add {t('addLabel', { label: getTranslation(modalCollection.labels.singular, i18n) })}
{' '}
{modalCollection.labels.singular}
</h1> </h1>
<Button <Button
icon="x" icon="x"
@@ -144,12 +143,12 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
</header> </header>
{moreThanOneAvailableCollection && ( {moreThanOneAvailableCollection && (
<div className={`${baseModalClass}__select-collection-wrap`}> <div className={`${baseModalClass}__select-collection-wrap`}>
<Label label="Select a Collection to Browse" /> <Label label={t('selectCollectionToBrowse')} />
<ReactSelect <ReactSelect
className={`${baseClass}__select-collection`} className={`${baseClass}__select-collection`}
value={modalCollectionOption} value={modalCollectionOption}
onChange={setModalCollectionOption} onChange={setModalCollectionOption}
options={availableCollections.map((coll) => ({ label: coll.labels.singular, value: coll.slug }))} options={availableCollections.map((coll) => ({ label: getTranslation(coll.labels.singular, i18n), value: coll.slug }))}
/> />
</div> </div>
)} )}
@@ -198,7 +197,7 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
- -
{data.totalPages > 1 ? data.limit : data.totalDocs} {data.totalPages > 1 ? data.limit : data.totalDocs}
{' '} {' '}
of {t('general:of')}
{' '} {' '}
{data.totalDocs} {data.totalDocs}
</div> </div>

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Transforms, Element } from 'slate'; import { Transforms, Element } from 'slate';
import { ReactEditor, useSlateStatic } from 'slate-react'; import { ReactEditor, useSlateStatic } from 'slate-react';
import { Modal } from '@faceless-ui/modal'; import { Modal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../../../../../../utilities/Auth'; import { useAuth } from '../../../../../../../utilities/Auth';
import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types'; import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types';
import buildStateFromSchema from '../../../../../../Form/buildStateFromSchema'; import buildStateFromSchema from '../../../../../../Form/buildStateFromSchema';
@@ -13,6 +14,7 @@ import Form from '../../../../../../Form';
import Submit from '../../../../../../Submit'; import Submit from '../../../../../../Submit';
import { Field } from '../../../../../../../../../fields/config/types'; import { Field } from '../../../../../../../../../fields/config/types';
import { useLocale } from '../../../../../../../utilities/Locale'; import { useLocale } from '../../../../../../../utilities/Locale';
import { getTranslation } from '../../../../../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -32,6 +34,7 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
const [initialState, setInitialState] = useState({}); const [initialState, setInitialState] = useState({});
const { user } = useAuth(); const { user } = useAuth();
const locale = useLocale(); const locale = useLocale();
const { t, i18n } = useTranslation('fields');
const handleUpdateEditData = useCallback((_, data) => { const handleUpdateEditData = useCallback((_, data) => {
const newNode = { const newNode = {
@@ -50,12 +53,12 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
useEffect(() => { useEffect(() => {
const awaitInitialState = async () => { const awaitInitialState = async () => {
const state = await buildStateFromSchema({ fieldSchema, data: { ...element?.fields || {} }, user, operation: 'update', locale }); const state = await buildStateFromSchema({ fieldSchema, data: { ...element?.fields || {} }, user, operation: 'update', locale, t });
setInitialState(state); setInitialState(state);
}; };
awaitInitialState(); awaitInitialState();
}, [fieldSchema, element.fields, user, locale]); }, [fieldSchema, element.fields, user, locale, t]);
return ( return (
<Modal <Modal
@@ -65,11 +68,7 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
<MinimalTemplate width="wide"> <MinimalTemplate width="wide">
<header className={`${baseClass}__header`}> <header className={`${baseClass}__header`}>
<h1> <h1>
Edit { t('editLabelData', { label: getTranslation(relatedCollectionConfig.labels.singular, i18n) }) }
{' '}
{relatedCollectionConfig.labels.singular}
{' '}
data
</h1> </h1>
<Button <Button
icon="x" icon="x"
@@ -90,7 +89,7 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
fieldSchema={fieldSchema} fieldSchema={fieldSchema}
/> />
<Submit> <Submit>
Save changes {t('saveChanges')}
</Submit> </Submit>
</Form> </Form>
</div> </div>

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import { Modal } from '@faceless-ui/modal'; import { Modal } from '@faceless-ui/modal';
import { Element, Transforms } from 'slate'; import { Element, Transforms } from 'slate';
import { ReactEditor, useSlateStatic } from 'slate-react'; import { ReactEditor, useSlateStatic } from 'slate-react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../../../../utilities/Config'; import { useConfig } from '../../../../../../../utilities/Config';
import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types'; import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types';
import usePayloadAPI from '../../../../../../../../hooks/usePayloadAPI'; import usePayloadAPI from '../../../../../../../../hooks/usePayloadAPI';
@@ -14,6 +15,7 @@ import UploadGallery from '../../../../../../../elements/UploadGallery';
import Paginator from '../../../../../../../elements/Paginator'; import Paginator from '../../../../../../../elements/Paginator';
import PerPage from '../../../../../../../elements/PerPage'; import PerPage from '../../../../../../../elements/PerPage';
import formatFields from '../../../../../../../views/collections/List/formatFields'; import formatFields from '../../../../../../../views/collections/List/formatFields';
import { getTranslation } from '../../../../../../../../../utilities/getTranslation';
import '../../addSwapModals.scss'; import '../../addSwapModals.scss';
@@ -29,11 +31,12 @@ type Props = {
export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelatedCollectionConfig, relatedCollectionConfig, slug }) => { export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelatedCollectionConfig, relatedCollectionConfig, slug }) => {
const { collections, serverURL, routes: { api } } = useConfig(); const { collections, serverURL, routes: { api } } = useConfig();
const editor = useSlateStatic(); const editor = useSlateStatic();
const { t, i18n } = useTranslation('upload');
const [modalCollection, setModalCollection] = React.useState(relatedCollectionConfig); const [modalCollection, setModalCollection] = React.useState(relatedCollectionConfig);
const [modalCollectionOption, setModalCollectionOption] = React.useState<{ label: string, value: string }>({ label: relatedCollectionConfig.labels.singular, value: relatedCollectionConfig.slug }); const [modalCollectionOption, setModalCollectionOption] = React.useState<{ label: string, value: string }>({ label: getTranslation(relatedCollectionConfig.labels.singular, i18n), value: relatedCollectionConfig.slug });
const [availableCollections] = React.useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship))); const [availableCollections] = React.useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
const [fields, setFields] = React.useState(() => formatFields(modalCollection)); const [fields, setFields] = React.useState(() => formatFields(modalCollection, t));
const [limit, setLimit] = React.useState<number>(); const [limit, setLimit] = React.useState<number>();
const [sort, setSort] = React.useState(null); const [sort, setSort] = React.useState(null);
@@ -82,9 +85,9 @@ export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelat
}, [setParams, page, sort, where, limit]); }, [setParams, page, sort, where, limit]);
React.useEffect(() => { React.useEffect(() => {
setFields(formatFields(modalCollection)); setFields(formatFields(modalCollection, t));
setLimit(modalCollection.admin.pagination.defaultLimit); setLimit(modalCollection.admin.pagination.defaultLimit);
}, [modalCollection]); }, [modalCollection, t]);
React.useEffect(() => { React.useEffect(() => {
setModalCollection(collections.find(({ slug: collectionSlug }) => modalCollectionOption.value === collectionSlug)); setModalCollection(collections.find(({ slug: collectionSlug }) => modalCollectionOption.value === collectionSlug));
@@ -98,9 +101,7 @@ export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelat
<MinimalTemplate width="wide"> <MinimalTemplate width="wide">
<header className={`${baseClass}__header`}> <header className={`${baseClass}__header`}>
<h1> <h1>
Choose {t('chooseLabel', { label: getTranslation(modalCollection.labels.singular, i18n) })}
{' '}
{modalCollection.labels.singular}
</h1> </h1>
<Button <Button
icon="x" icon="x"
@@ -113,12 +114,12 @@ export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelat
{ {
moreThanOneAvailableCollection && ( moreThanOneAvailableCollection && (
<div className={`${baseClass}__select-collection-wrap`}> <div className={`${baseClass}__select-collection-wrap`}>
<Label label="Select a Collection to Browse" /> <Label label={t('selectCollectionToBrowse')} />
<ReactSelect <ReactSelect
className={`${baseClass}__select-collection`} className={`${baseClass}__select-collection`}
value={modalCollectionOption} value={modalCollectionOption}
onChange={setModalCollectionOption} onChange={setModalCollectionOption}
options={availableCollections.map((coll) => ({ label: coll.labels.singular, value: coll.slug }))} options={availableCollections.map((coll) => ({ label: getTranslation(coll.labels.singular, i18n), value: coll.slug }))}
/> />
</div> </div>
) )
@@ -165,7 +166,7 @@ export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelat
- -
{data.totalPages > 1 ? data.limit : data.totalDocs} {data.totalPages > 1 ? data.limit : data.totalDocs}
{' '} {' '}
of {t('general:of')}
{' '} {' '}
{data.totalDocs} {data.totalDocs}
</div> </div>

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import { useModal } from '@faceless-ui/modal'; import { useModal } from '@faceless-ui/modal';
import { Transforms } from 'slate'; import { Transforms } from 'slate';
import { ReactEditor, useSlateStatic, useFocused, useSelected } from 'slate-react'; import { ReactEditor, useSlateStatic, useFocused, useSelected } from 'slate-react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../../../utilities/Config'; import { useConfig } from '../../../../../../utilities/Config';
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI'; import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
import FileGraphic from '../../../../../../graphics/File'; import FileGraphic from '../../../../../../graphics/File';
@@ -10,6 +11,7 @@ import Button from '../../../../../../elements/Button';
import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types'; import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types';
import { SwapUploadModal } from './SwapUploadModal'; import { SwapUploadModal } from './SwapUploadModal';
import { EditModal } from './EditModal'; import { EditModal } from './EditModal';
import { getTranslation } from '../../../../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -25,6 +27,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
const { collections, serverURL, routes: { api } } = useConfig(); const { collections, serverURL, routes: { api } } = useConfig();
const [modalToRender, setModalToRender] = useState(undefined); const [modalToRender, setModalToRender] = useState(undefined);
const [relatedCollection, setRelatedCollection] = useState<SanitizedCollectionConfig>(() => collections.find((coll) => coll.slug === relationTo)); const [relatedCollection, setRelatedCollection] = useState<SanitizedCollectionConfig>(() => collections.find((coll) => coll.slug === relationTo));
const { t, i18n } = useTranslation('fields');
const editor = useSlateStatic(); const editor = useSlateStatic();
const selected = useSelected(); const selected = useSelected();
@@ -85,7 +88,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
</div> </div>
<div className={`${baseClass}__topRowRightPanel`}> <div className={`${baseClass}__topRowRightPanel`}>
<div className={`${baseClass}__collectionLabel`}> <div className={`${baseClass}__collectionLabel`}>
{relatedCollection.labels.singular} {getTranslation(relatedCollection.labels.singular, i18n)}
</div> </div>
<div className={`${baseClass}__actions`}> <div className={`${baseClass}__actions`}>
{fieldSchema && ( {fieldSchema && (
@@ -98,7 +101,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
e.preventDefault(); e.preventDefault();
setModalToRender('edit'); setModalToRender('edit');
}} }}
tooltip="Edit" tooltip={t('general:edit')}
/> />
)} )}
<Button <Button
@@ -110,7 +113,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
e.preventDefault(); e.preventDefault();
setModalToRender('swap'); setModalToRender('swap');
}} }}
tooltip="Swap Upload" tooltip={t('swapUpload')}
/> />
<Button <Button
icon="x" icon="x"
@@ -121,7 +124,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
e.preventDefault(); e.preventDefault();
removeUpload(); removeUpload();
}} }}
tooltip="Remove Upload" tooltip={t('removeUpload')}
/> />
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import Label from '../../Label'; import Label from '../../Label';
import Error from '../../Error'; import Error from '../../Error';
import FieldDescription from '../../FieldDescription'; import FieldDescription from '../../FieldDescription';
@@ -6,7 +7,7 @@ import { OptionObject, SelectField } from '../../../../../fields/config/types';
import { Description } from '../../FieldDescription/types'; import { Description } from '../../FieldDescription/types';
import ReactSelect from '../../../elements/ReactSelect'; import ReactSelect from '../../../elements/ReactSelect';
import { Value as ReactSelectValue } from '../../../elements/ReactSelect/types'; import { Value as ReactSelectValue } from '../../../elements/ReactSelect/types';
// import { FieldType } from '../../useField/types'; import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -48,6 +49,8 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
isClearable, isClearable,
} = props; } = props;
const { i18n } = useTranslation();
const classes = [ const classes = [
'field-type', 'field-type',
'select', 'select',
@@ -87,7 +90,7 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
value={valueToRender as ReactSelectValue} value={valueToRender as ReactSelectValue}
showError={showError} showError={showError}
isDisabled={readOnly} isDisabled={readOnly}
options={options} options={options.map((option) => ({ ...option, label: getTranslation(option.label, i18n) }))}
isMulti={hasMany} isMulti={hasMany}
isSortable={isSortable} isSortable={isSortable}
isClearable={isClearable} isClearable={isClearable}

View File

@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import RenderFields from '../../RenderFields'; import RenderFields from '../../RenderFields';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import { Props } from './types'; import { Props } from './types';
@@ -7,6 +8,7 @@ import FieldDescription from '../../FieldDescription';
import toKebabCase from '../../../../../utilities/toKebabCase'; import toKebabCase from '../../../../../utilities/toKebabCase';
import { useCollapsible } from '../../../elements/Collapsible/provider'; import { useCollapsible } from '../../../elements/Collapsible/provider';
import { TabsProvider } from './provider'; import { TabsProvider } from './provider';
import { getTranslation } from '../../../../../utilities/getTranslation';
import { usePreferences } from '../../../utilities/Preferences'; import { usePreferences } from '../../../utilities/Preferences';
import { DocumentPreferences } from '../../../../../preferences/types'; import { DocumentPreferences } from '../../../../../preferences/types';
import { useDocumentInfo } from '../../../utilities/DocumentInfo'; import { useDocumentInfo } from '../../../utilities/DocumentInfo';
@@ -30,6 +32,7 @@ const TabsField: React.FC<Props> = (props) => {
const { getPreference, setPreference } = usePreferences(); const { getPreference, setPreference } = usePreferences();
const { preferencesKey } = useDocumentInfo(); const { preferencesKey } = useDocumentInfo();
const { i18n } = useTranslation();
const isWithinCollapsible = useCollapsible(); const isWithinCollapsible = useCollapsible();
const [activeTabIndex, setActiveTabIndex] = useState<number>(0); const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
@@ -93,10 +96,10 @@ const TabsField: React.FC<Props> = (props) => {
activeTabIndex === tabIndex && `${baseClass}__tab-button--active`, activeTabIndex === tabIndex && `${baseClass}__tab-button--active`,
].filter(Boolean).join(' ')} ].filter(Boolean).join(' ')}
onClick={() => { onClick={() => {
handleTabChange(tabIndex) handleTabChange(tabIndex);
}} }}
> >
{tab.label ? tab.label : (tabHasName(tab) && tab.name)} {tab.label ? getTranslation(tab.label, i18n) : (tabHasName(tab) && tab.name)}
</button> </button>
); );
})} })}

View File

@@ -1,9 +1,11 @@
import React, { ChangeEvent } from 'react'; import React, { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import Label from '../../Label'; import Label from '../../Label';
import Error from '../../Error'; import Error from '../../Error';
import FieldDescription from '../../FieldDescription'; import FieldDescription from '../../FieldDescription';
import { TextField } from '../../../../../fields/config/types'; import { TextField } from '../../../../../fields/config/types';
import { Description } from '../../FieldDescription/types'; import { Description } from '../../FieldDescription/types';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -17,7 +19,7 @@ export type TextInputProps = Omit<TextField, 'type'> & {
description?: Description description?: Description
onChange?: (e: ChangeEvent<HTMLInputElement>) => void onChange?: (e: ChangeEvent<HTMLInputElement>) => void
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement> onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
placeholder?: string placeholder?: Record<string, string> | string
style?: React.CSSProperties style?: React.CSSProperties
className?: string className?: string
width?: string width?: string
@@ -43,6 +45,8 @@ const TextInput: React.FC<TextInputProps> = (props) => {
inputRef, inputRef,
} = props; } = props;
const { i18n } = useTranslation();
const classes = [ const classes = [
'field-type', 'field-type',
'text', 'text',
@@ -75,11 +79,12 @@ const TextInput: React.FC<TextInputProps> = (props) => {
onChange={onChange} onChange={onChange}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
disabled={readOnly} disabled={readOnly}
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
type="text" type="text"
name={path} name={path}
/> />
<FieldDescription <FieldDescription
className={`field-description-${path.replace(/\./gi, '__')}`}
value={value} value={value}
description={description} description={description}
/> />

View File

@@ -1,9 +1,11 @@
import React, { ChangeEvent } from 'react'; import React, { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import Label from '../../Label'; import Label from '../../Label';
import Error from '../../Error'; import Error from '../../Error';
import FieldDescription from '../../FieldDescription'; import FieldDescription from '../../FieldDescription';
import { TextareaField } from '../../../../../fields/config/types'; import { TextareaField } from '../../../../../fields/config/types';
import { Description } from '../../FieldDescription/types'; import { Description } from '../../FieldDescription/types';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -16,7 +18,7 @@ export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
value?: string value?: string
description?: Description description?: Description
onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
placeholder?: string placeholder?: Record<string, string> | string
style?: React.CSSProperties style?: React.CSSProperties
className?: string className?: string
width?: string width?: string
@@ -41,6 +43,8 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
rows, rows,
} = props; } = props;
const { i18n } = useTranslation();
const classes = [ const classes = [
'field-type', 'field-type',
'textarea', 'textarea',
@@ -78,7 +82,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
value={value || ''} value={value || ''}
onChange={onChange} onChange={onChange}
disabled={readOnly} disabled={readOnly}
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
name={path} name={path}
rows={rows} rows={rows}
/> />

View File

@@ -1,9 +1,11 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import useField from '../../useField'; import useField from '../../useField';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import { textarea } from '../../../../../fields/validations'; import { textarea } from '../../../../../fields/validations';
import { Props } from './types'; import { Props } from './types';
import TextareaInput from './Input'; import TextareaInput from './Input';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -28,6 +30,8 @@ const Textarea: React.FC<Props> = (props) => {
label, label,
} = props; } = props;
const { i18n } = useTranslation();
const path = pathFromProps || name; const path = pathFromProps || name;
const memoizedValidate = useCallback((value, options) => { const memoizedValidate = useCallback((value, options) => {
@@ -57,7 +61,7 @@ const Textarea: React.FC<Props> = (props) => {
required={required} required={required}
label={label} label={label}
value={value as string} value={value as string}
placeholder={placeholder} placeholder={getTranslation(placeholder, i18n)}
readOnly={readOnly} readOnly={readOnly}
style={style} style={style}
className={className} className={className}

View File

@@ -1,5 +1,6 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Modal, useModal } from '@faceless-ui/modal'; import { Modal, useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../utilities/Config'; import { useConfig } from '../../../../utilities/Config';
import { useAuth } from '../../../../utilities/Auth'; import { useAuth } from '../../../../utilities/Auth';
import MinimalTemplate from '../../../../templates/Minimal'; import MinimalTemplate from '../../../../templates/Minimal';
@@ -9,6 +10,7 @@ import RenderFields from '../../../RenderFields';
import FormSubmit from '../../../Submit'; import FormSubmit from '../../../Submit';
import Upload from '../../../../views/collections/Edit/Upload'; import Upload from '../../../../views/collections/Edit/Upload';
import ViewDescription from '../../../../elements/ViewDescription'; import ViewDescription from '../../../../elements/ViewDescription';
import { getTranslation } from '../../../../../../utilities/getTranslation';
import { Props } from './types'; import { Props } from './types';
import './index.scss'; import './index.scss';
@@ -31,6 +33,7 @@ const AddUploadModal: React.FC<Props> = (props) => {
const { permissions } = useAuth(); const { permissions } = useAuth();
const { serverURL, routes: { api } } = useConfig(); const { serverURL, routes: { api } } = useConfig();
const { toggleModal } = useModal(); const { toggleModal } = useModal();
const { t, i18n } = useTranslation('fields');
const onSuccess = useCallback((json) => { const onSuccess = useCallback((json) => {
toggleModal(slug); toggleModal(slug);
@@ -59,11 +62,9 @@ const AddUploadModal: React.FC<Props> = (props) => {
<header className={`${baseClass}__header`}> <header className={`${baseClass}__header`}>
<div> <div>
<h1> <h1>
New {t('newLabel', { label: getTranslation(collection.labels.singular, i18n) })}
{' '}
{collection.labels.singular}
</h1> </h1>
<FormSubmit>Save</FormSubmit> <FormSubmit>{t('general:save')}</FormSubmit>
<Button <Button
icon="x" icon="x"
round round

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useModal } from '@faceless-ui/modal'; import { useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import Button from '../../../elements/Button'; import Button from '../../../elements/Button';
import Label from '../../Label'; import Label from '../../Label';
import Error from '../../Error'; import Error from '../../Error';
@@ -12,6 +13,7 @@ import AddModal from './Add';
import SelectExistingModal from './SelectExisting'; import SelectExistingModal from './SelectExisting';
import { SanitizedCollectionConfig } from '../../../../../collections/config/types'; import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { useEditDepth, EditDepthContext } from '../../../utilities/EditDepth'; import { useEditDepth, EditDepthContext } from '../../../utilities/EditDepth';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -60,6 +62,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
} = props; } = props;
const { toggleModal, modalState } = useModal(); const { toggleModal, modalState } = useModal();
const { t, i18n } = useTranslation('fields');
const editDepth = useEditDepth(); const editDepth = useEditDepth();
const addModalSlug = `${path}-add-depth-${editDepth}`; const addModalSlug = `${path}-add-depth-${editDepth}`;
@@ -80,7 +83,12 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
useEffect(() => { useEffect(() => {
if (typeof value === 'string' && value !== '') { if (typeof value === 'string' && value !== '') {
const fetchFile = async () => { const fetchFile = async () => {
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`, { credentials: 'include' }); const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
});
if (response.ok) { if (response.ok) {
const json = await response.json(); const json = await response.json();
setFile(json); setFile(json);
@@ -99,6 +107,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
relationTo, relationTo,
api, api,
serverURL, serverURL,
i18n,
]); ]);
useEffect(() => { useEffect(() => {
@@ -144,9 +153,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
setModalToRender(addModalSlug); setModalToRender(addModalSlug);
}} }}
> >
Upload new {t('uploadNewLabel', { label: getTranslation(collection.labels.singular, i18n) })}
{' '}
{collection.labels.singular}
</Button> </Button>
<Button <Button
buttonStyle="secondary" buttonStyle="secondary"
@@ -155,7 +162,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
setModalToRender(selectExistingModalSlug); setModalToRender(selectExistingModalSlug);
}} }}
> >
Choose from existing {t('chooseFromExisting')}
</Button> </Button>
</div> </div>
)} )}

View File

@@ -1,6 +1,7 @@
import React, { Fragment, useState, useEffect } from 'react'; import React, { Fragment, useState, useEffect } from 'react';
import equal from 'deep-equal'; import equal from 'deep-equal';
import { Modal, useModal } from '@faceless-ui/modal'; import { Modal, useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../utilities/Config'; import { useConfig } from '../../../../utilities/Config';
import { useAuth } from '../../../../utilities/Auth'; import { useAuth } from '../../../../utilities/Auth';
import { Where } from '../../../../../../types'; import { Where } from '../../../../../../types';
@@ -17,6 +18,7 @@ import { getFilterOptionsQuery } from '../../getFilterOptionsQuery';
import { useDocumentInfo } from '../../../../utilities/DocumentInfo'; import { useDocumentInfo } from '../../../../utilities/DocumentInfo';
import { useForm } from '../../../Form/context'; import { useForm } from '../../../Form/context';
import ViewDescription from '../../../../elements/ViewDescription'; import ViewDescription from '../../../../elements/ViewDescription';
import { getTranslation } from '../../../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
@@ -45,7 +47,8 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
const { user } = useAuth(); const { user } = useAuth();
const { getData, getSiblingData } = useForm(); const { getData, getSiblingData } = useForm();
const { toggleModal, isModalOpen } = useModal(); const { toggleModal, isModalOpen } = useModal();
const [fields] = useState(() => formatFields(collection)); const { t, i18n } = useTranslation('fields');
const [fields] = useState(() => formatFields(collection, t));
const [limit, setLimit] = useState(defaultLimit); const [limit, setLimit] = useState(defaultLimit);
const [sort, setSort] = useState(null); const [sort, setSort] = useState(null);
const [where, setWhere] = useState(null); const [where, setWhere] = useState(null);
@@ -105,10 +108,7 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
<header className={`${baseClass}__header`}> <header className={`${baseClass}__header`}>
<div> <div>
<h1> <h1>
{' '} {t('selectExistingLabel', { label: getTranslation(collection.labels.singular, i18n) })}
Select existing
{' '}
{collection.labels.singular}
</h1> </h1>
<Button <Button
icon="x" icon="x"
@@ -163,7 +163,7 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
- -
{data.totalPages > 1 ? data.limit : data.totalDocs} {data.totalPages > 1 ? data.limit : data.totalDocs}
{' '} {' '}
of {t('general:of')}
{' '} {' '}
{data.totalDocs} {data.totalDocs}
</div> </div>

View File

@@ -1,4 +1,5 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../utilities/Auth'; import { useAuth } from '../../utilities/Auth';
import { useFormProcessing, useFormSubmitted, useFormModified, useForm, useFormFields } from '../Form/context'; import { useFormProcessing, useFormSubmitted, useFormModified, useForm, useFormFields } from '../Form/context';
import { Options, FieldType } from './types'; import { Options, FieldType } from './types';
@@ -23,6 +24,7 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
const operation = useOperation(); const operation = useOperation();
const field = useFormFields(([fields]) => fields[path]); const field = useFormFields(([fields]) => fields[path]);
const dispatchField = useFormFields(([_, dispatch]) => dispatch); const dispatchField = useFormFields(([_, dispatch]) => dispatch);
const { t } = useTranslation();
const { getData, getSiblingData, setModified } = useForm(); const { getData, getSiblingData, setModified } = useForm();
@@ -92,6 +94,7 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
data: getData(), data: getData(),
siblingData: getSiblingData(path), siblingData: getSiblingData(path),
operation, operation,
t,
}; };
const validationResult = typeof validate === 'function' ? await validate(value, validateOptions) : true; const validationResult = typeof validate === 'function' ? await validate(value, validateOptions) : true;

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import NavigationPrompt from 'react-router-navigation-prompt'; import NavigationPrompt from 'react-router-navigation-prompt';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../utilities/Auth'; import { useAuth } from '../../utilities/Auth';
import { useFormModified } from '../../forms/Form/context'; import { useFormModified } from '../../forms/Form/context';
import MinimalTemplate from '../../templates/Minimal'; import MinimalTemplate from '../../templates/Minimal';
@@ -12,24 +13,25 @@ const modalSlug = 'leave-without-saving';
const LeaveWithoutSaving: React.FC = () => { const LeaveWithoutSaving: React.FC = () => {
const modified = useFormModified(); const modified = useFormModified();
const { user } = useAuth(); const { user } = useAuth();
const { t } = useTranslation('general');
return ( return (
<NavigationPrompt when={Boolean(modified && user)}> <NavigationPrompt when={Boolean(modified && user)}>
{({ onConfirm, onCancel }) => ( {({ onConfirm, onCancel }) => (
<div className={modalSlug}> <div className={modalSlug}>
<MinimalTemplate className={`${modalSlug}__template`}> <MinimalTemplate className={`${modalSlug}__template`}>
<h1>Leave without saving</h1> <h1>{t('leaveWithoutSaving')}</h1>
<p>Your changes have not been saved. If you leave now, you will lose your changes.</p> <p>{t('changesNotSaved')}</p>
<Button <Button
onClick={onCancel} onClick={onCancel}
buttonStyle="secondary" buttonStyle="secondary"
> >
Stay on this page {t('stayOnThisPage')}
</Button> </Button>
<Button <Button
onClick={onConfirm} onClick={onConfirm}
> >
Leave anyway {t('leaveAnyway')}
</Button> </Button>
</MinimalTemplate> </MinimalTemplate>
</div> </div>

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useModal, Modal } from '@faceless-ui/modal'; import { useModal, Modal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import MinimalTemplate from '../../templates/Minimal'; import MinimalTemplate from '../../templates/Minimal';
import Button from '../../elements/Button'; import Button from '../../elements/Button';
@@ -23,6 +24,7 @@ const StayLoggedInModal: React.FC<Props> = (props) => {
} }
} = config; } = config;
const { toggleModal } = useModal(); const { toggleModal } = useModal();
const { t } = useTranslation('authentication');
return ( return (
<Modal <Modal
@@ -30,8 +32,8 @@ const StayLoggedInModal: React.FC<Props> = (props) => {
slug="stay-logged-in" slug="stay-logged-in"
> >
<MinimalTemplate className={`${baseClass}__template`}> <MinimalTemplate className={`${baseClass}__template`}>
<h1>Stay logged in</h1> <h1>{t('stayLoggedIn')}</h1>
<p>You haven&apos;t been active in a little while and will shortly be automatically logged out for your own security. Would you like to stay logged in?</p> <p>{t('youAreInactive')}</p>
<div className={`${baseClass}__actions`}> <div className={`${baseClass}__actions`}>
<Button <Button
buttonStyle="secondary" buttonStyle="secondary"
@@ -40,14 +42,14 @@ const StayLoggedInModal: React.FC<Props> = (props) => {
history.push(`${admin}${logoutRoute}`); history.push(`${admin}${logoutRoute}`);
}} }}
> >
Log out {t('logOut')}
</Button> </Button>
<Button onClick={() => { <Button onClick={() => {
refreshCookie(); refreshCookie();
toggleModal(modalSlug); toggleModal(modalSlug);
}} }}
> >
Stay logged in {t('stayLoggedIn')}
</Button> </Button>
</div> </div>
</MinimalTemplate> </MinimalTemplate>

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import DefaultNav from '../../elements/Nav'; import DefaultNav from '../../elements/Nav';
import RenderCustomComponent from '../../utilities/RenderCustomComponent'; import RenderCustomComponent from '../../utilities/RenderCustomComponent';
@@ -19,6 +20,7 @@ const Default: React.FC<Props> = ({ children, className }) => {
}, },
} = {}, } = {},
} = useConfig(); } = useConfig();
const { t } = useTranslation('general');
const classes = [ const classes = [
baseClass, baseClass,
@@ -28,9 +30,9 @@ const Default: React.FC<Props> = ({ children, className }) => {
return ( return (
<div className={classes}> <div className={classes}>
<Meta <Meta
title="Dashboard" title={t('dashboard')}
description="Dashboard for Payload CMS" description={`${t('dashboard')} Payload CMS`}
keywords="Dashboard, Payload, CMS" keywords={`${t('dashboard')}, Payload CMS`}
/> />
<RenderCustomComponent <RenderCustomComponent
DefaultComponent={DefaultNav} DefaultComponent={DefaultNav}

View File

@@ -4,6 +4,7 @@ import React, {
import jwtDecode from 'jwt-decode'; import jwtDecode from 'jwt-decode';
import { useLocation, useHistory } from 'react-router-dom'; import { useLocation, useHistory } from 'react-router-dom';
import { useModal } from '@faceless-ui/modal'; import { useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import { User, Permissions } from '../../../../auth/types'; import { User, Permissions } from '../../../../auth/types';
import { useConfig } from '../Config'; import { useConfig } from '../Config';
import { requests } from '../../../api'; import { requests } from '../../../api';
@@ -38,7 +39,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const [permissions, setPermissions] = useState<Permissions>(); const [permissions, setPermissions] = useState<Permissions>();
const { i18n } = useTranslation();
const { openModal, closeAllModals } = useModal(); const { openModal, closeAllModals } = useModal();
const [lastLocationChange, setLastLocationChange] = useState(0); const [lastLocationChange, setLastLocationChange] = useState(0);
const debouncedLocationChange = useDebounce(lastLocationChange, 10000); const debouncedLocationChange = useDebounce(lastLocationChange, 10000);
@@ -51,7 +52,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (exp && remainingTime < 120) { if (exp && remainingTime < 120) {
setTimeout(async () => { setTimeout(async () => {
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`); const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (request.status === 200) { if (request.status === 200) {
const json = await request.json(); const json = await request.json();
@@ -62,7 +67,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
} }
}, 1000); }, 1000);
} }
}, [setUser, push, exp, admin, api, serverURL, userSlug]); }, [exp, serverURL, api, userSlug, push, admin, logoutInactivityRoute, i18n]);
const setToken = useCallback((token: string) => { const setToken = useCallback((token: string) => {
const decoded = jwtDecode<User>(token); const decoded = jwtDecode<User>(token);
@@ -79,7 +84,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
// On mount, get user and set // On mount, get user and set
useEffect(() => { useEffect(() => {
const fetchMe = async () => { const fetchMe = async () => {
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`); const request = await requests.get(`${serverURL}${api}/${userSlug}/me`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (request.status === 200) { if (request.status === 200) {
const json = await request.json(); const json = await request.json();
@@ -93,7 +102,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}; };
fetchMe(); fetchMe();
}, [setToken, api, serverURL, userSlug]); }, [i18n, setToken, api, serverURL, userSlug]);
// When location changes, refresh cookie // When location changes, refresh cookie
useEffect(() => { useEffect(() => {
@@ -109,7 +118,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
// When user changes, get new access // When user changes, get new access
useEffect(() => { useEffect(() => {
async function getPermissions() { async function getPermissions() {
const request = await requests.get(`${serverURL}${api}/access`); const request = await requests.get(`${serverURL}${api}/access`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (request.status === 200) { if (request.status === 200) {
const json: Permissions = await request.json(); const json: Permissions = await request.json();
@@ -120,7 +133,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (id) { if (id) {
getPermissions(); getPermissions();
} }
}, [id, api, serverURL]); }, [i18n, id, api, serverURL]);
useEffect(() => { useEffect(() => {
let reminder: ReturnType<typeof setTimeout>; let reminder: ReturnType<typeof setTimeout>;
@@ -154,7 +167,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
return () => { return () => {
if (forceLogOut) clearTimeout(forceLogOut); if (forceLogOut) clearTimeout(forceLogOut);
}; };
}, [exp, push, closeAllModals, admin]); }, [exp, push, closeAllModals, admin, i18n, logoutInactivityRoute]);
return ( return (
<Context.Provider value={{ <Context.Provider value={{

Some files were not shown because too many files have changed in this diff Show More