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

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

View File

@@ -2,6 +2,7 @@ import React, { Suspense, lazy, useState, useEffect } from 'react';
import {
Route, Switch, withRouter, Redirect,
} from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from './utilities/Auth';
import { useConfig } from './utilities/Config';
import List from './views/collections/List';
@@ -29,6 +30,7 @@ const Account = lazy(() => import('./views/Account'));
const Routes = () => {
const [initialized, setInitialized] = useState(null);
const { user, permissions, refreshCookie } = useAuth();
const { i18n } = useTranslation();
const canAccessAdmin = permissions?.canAccessAdmin;
@@ -54,7 +56,11 @@ const Routes = () => {
const { slug } = userCollection;
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) {
setInitialized(data.initialized);
}
@@ -62,7 +68,7 @@ const Routes = () => {
} else {
setInitialized(true);
}
}, [routes, userCollection]);
}, [i18n.language, routes, userCollection]);
return (
<Suspense fallback={<Loading />}>

View File

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

View File

@@ -1,7 +1,7 @@
import { formatDistance } from 'date-fns';
import { useHistory } from 'react-router-dom';
import { toast } from 'react-toastify';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config';
import { useFormModified, useAllFormFields } from '../../forms/Form/context';
import { useLocale } from '../../utilities/Locale';
@@ -21,6 +21,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
const modified = useFormModified();
const locale = useLocale();
const { replace } = useHistory();
const { t, i18n } = useTranslation('version');
let interval = 800;
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',
headers: {
'Content-Type': 'application/json',
'Accept-Language': i18n.language,
},
body: JSON.stringify({}),
});
@@ -54,9 +56,9 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
},
});
} 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(() => {
// 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',
headers: {
'Content-Type': 'application/json',
'Accept-Language': i18n.language,
},
body: JSON.stringify(body),
});
@@ -114,7 +117,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
};
autosave();
}, [debouncedFields, modified, serverURL, api, collection, global, id, getVersions, locale]);
}, [i18n, debouncedFields, modified, serverURL, api, collection, global, id, getVersions, locale]);
useEffect(() => {
if (versions?.docs?.[0]) {
@@ -126,12 +129,12 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
return (
<div className={baseClass}>
{saving && 'Saving...'}
{saving && t('saving')}
{(!saving && lastSaved) && (
<React.Fragment>
Last saved&nbsp;
{formatDistance(new Date(), new Date(lastSaved))}
&nbsp;ago
{t('lastSavedAgo', {
distance: Math.round((Number(new Date(lastSaved)) - Number(new Date())) / 1000 / 60),
})}
</React.Fragment>
)}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import React, { useReducer, useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../utilities/Config';
import { Props, Option, ValueWithRelation, GetResults } from './types';
import optionsReducer from './optionsReducer';
@@ -32,11 +33,12 @@ const RelationshipField: React.FC<Props> = (props) => {
const [errorLoading, setErrorLoading] = useState('');
const [hasLoadedFirstOptions, setHasLoadedFirstOptions] = useState(false);
const debouncedSearch = useDebounce(search, 300);
const { t, i18n } = useTranslation('general');
const addOptions = useCallback((data, relation) => {
const collection = collections.find((coll) => coll.slug === relation);
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection });
}, [collections, hasMultipleRelations]);
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection, i18n });
}, [collections, hasMultipleRelations, i18n]);
const getResults = useCallback<GetResults>(async ({
lastFullyLoadedRelation: lastFullyLoadedRelationArg,
@@ -60,10 +62,15 @@ const RelationshipField: React.FC<Props> = (props) => {
const fieldToSearch = collection?.admin?.useAsTitle || 'id';
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) {
const data: PaginatedDocs<any> = await response.json();
const data: PaginatedDocs = await response.json();
if (data.docs.length > 0) {
resultsFetched += data.docs.length;
addOptions(data, relation);
@@ -80,12 +87,12 @@ const RelationshipField: React.FC<Props> = (props) => {
}
}
} else {
setErrorLoading('An error has occurred.');
setErrorLoading(t('errors:unspecific'));
}
}
}, Promise.resolve());
}
}, [addOptions, api, collections, serverURL, errorLoading, relationTo]);
}, [i18n, relationTo, errorLoading, collections, serverURL, api, addOptions, t]);
const findOptionsByValue = useCallback((): Option | Option[] => {
if (value) {
@@ -152,16 +159,21 @@ const RelationshipField: React.FC<Props> = (props) => {
const addOptionByID = useCallback(async (id, relation) => {
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) {
const data = await response.json();
addOptions({ docs: [data] }, relation);
} 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
@@ -171,13 +183,14 @@ const RelationshipField: React.FC<Props> = (props) => {
dispatchOptions({
type: 'CLEAR',
required: true,
i18n,
});
setHasLoadedFirstOptions(true);
setLastLoadedPage(1);
setLastFullyLoadedRelation(-1);
getResults({ search: debouncedSearch });
}, [getResults, debouncedSearch, relationTo]);
}, [getResults, debouncedSearch, relationTo, i18n]);
// ///////////////////////////
// Format options once first options have been retrieved
@@ -224,7 +237,7 @@ const RelationshipField: React.FC<Props> = (props) => {
<div className={classes}>
{!errorLoading && (
<ReactSelect
placeholder="Select a value"
placeholder={t('selectValue')}
onInputChange={handleInputChange}
onChange={(selected) => {
if (hasMany) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ export type DescriptionFunction = (value?: unknown) => string
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 = {
description?: Description

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ export type RowLabelFunction = (args: RowLabelArgs) => string
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 {
return React.isValidElement(label);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import React, {
} from 'react';
import equal from 'deep-equal';
import qs from 'qs';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../utilities/Config';
import { useAuth } from '../../../utilities/Auth';
import withCondition from '../../withCondition';
@@ -62,12 +63,13 @@ const Relationship: React.FC<Props> = (props) => {
collections,
} = useConfig();
const { t, i18n } = useTranslation('fields');
const { id } = useDocumentInfo();
const { user, permissions } = useAuth();
const [fields] = useAllFormFields();
const formProcessing = useFormProcessing();
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 [lastLoadedPage, setLastLoadedPage] = useState(1);
const [errorLoading, setErrorLoading] = useState('');
@@ -159,13 +161,18 @@ const Relationship: React.FC<Props> = (props) => {
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) {
const data: PaginatedDocs<unknown> = await response.json();
if (data.docs.length > 0) {
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);
if (!data.nextPage) {
@@ -181,9 +188,9 @@ const Relationship: React.FC<Props> = (props) => {
} else if (response.status === 403) {
setLastFullyLoadedRelation(relations.indexOf(relation));
lastLoadedPageToUse = 1;
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort, ids: relationMap[relation] });
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort, ids: relationMap[relation], i18n });
} else {
setErrorLoading('An error has occurred.');
setErrorLoading(t('error:unspecific'));
}
}
}, Promise.resolve());
@@ -198,6 +205,8 @@ const Relationship: React.FC<Props> = (props) => {
serverURL,
api,
hasMultipleRelations,
t,
i18n,
]);
const findOptionsByValue = useCallback((): Option | Option[] => {
@@ -295,13 +304,18 @@ const Relationship: React.FC<Props> = (props) => {
};
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);
if (response.ok) {
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) {
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);
}
}, [hasMany, hasMultipleRelations, relationTo, initialValue, hasLoadedValueOptions, errorLoading, collections, api, serverURL]);
}, [hasMany, hasMultipleRelations, relationTo, initialValue, hasLoadedValueOptions, errorLoading, collections, api, serverURL, i18n]);
useEffect(() => {
if (!filterOptions) return;

View File

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

View File

@@ -1,3 +1,4 @@
import i18n from 'i18next';
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { RelationshipField } from '../../../../../fields/config/types';
@@ -23,6 +24,7 @@ type ADD = {
collection: SanitizedCollectionConfig
sort?: boolean
ids?: unknown[]
i18n: typeof i18n
}
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 { ReactEditor, Editable, withReact, Slate } from 'slate-react';
import { HistoryEditor, withHistory } from 'slate-history';
import { useTranslation } from 'react-i18next';
import { richText } from '../../../../../fields/validations';
import useField from '../../useField';
import withCondition from '../../withCondition';
@@ -21,6 +22,7 @@ import { RichTextElement, RichTextLeaf } from '../../../../../fields/config/type
import listTypes from './elements/listTypes';
import mergeCustomFunctions from './mergeCustomFunctions';
import withEnterBreakOut from './plugins/withEnterBreakOut';
import { getTranslation } from '../../../../../utilities/getTranslation';
import './index.scss';
@@ -65,6 +67,7 @@ const RichText: React.FC<Props> = (props) => {
const path = pathFromProps || name;
const { i18n } = useTranslation();
const [loaded, setLoaded] = useState(false);
const [enabledElements, setEnabledElements] = useState({});
const [enabledLeaves, setEnabledLeaves] = useState({});
@@ -308,7 +311,7 @@ const RichText: React.FC<Props> = (props) => {
className={`${baseClass}__input`}
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder={placeholder}
placeholder={getTranslation(placeholder, i18n)}
spellCheck
readOnly={readOnly}
onKeyDown={(event) => {

View File

@@ -2,6 +2,7 @@ import React, { Fragment, useState } from 'react';
import { ReactEditor, useSlate } from 'slate-react';
import { Transforms, Editor, Range } from 'slate';
import { useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import ElementButton from '../Button';
import { unwrapLink } from './utilities';
import LinkIcon from '../../../../../icons/Link';
@@ -22,6 +23,7 @@ export const LinkButton = ({ fieldProps }) => {
const modalSlug = `${baseModalSlug}-${fieldProps.path}`;
const { t } = useTranslation();
const config = useConfig();
const editor = useSlate();
const { user } = useAuth();
@@ -71,7 +73,7 @@ export const LinkButton = ({ fieldProps }) => {
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);
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useModal, Modal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config';
import MinimalTemplate from '../../templates/Minimal';
import Button from '../../elements/Button';
@@ -23,6 +24,7 @@ const StayLoggedInModal: React.FC<Props> = (props) => {
}
} = config;
const { toggleModal } = useModal();
const { t } = useTranslation('authentication');
return (
<Modal
@@ -30,8 +32,8 @@ const StayLoggedInModal: React.FC<Props> = (props) => {
slug="stay-logged-in"
>
<MinimalTemplate className={`${baseClass}__template`}>
<h1>Stay logged in</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>
<h1>{t('stayLoggedIn')}</h1>
<p>{t('youAreInactive')}</p>
<div className={`${baseClass}__actions`}>
<Button
buttonStyle="secondary"
@@ -40,14 +42,14 @@ const StayLoggedInModal: React.FC<Props> = (props) => {
history.push(`${admin}${logoutRoute}`);
}}
>
Log out
{t('logOut')}
</Button>
<Button onClick={() => {
refreshCookie();
toggleModal(modalSlug);
}}
>
Stay logged in
{t('stayLoggedIn')}
</Button>
</div>
</MinimalTemplate>

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import React, {
createContext, useCallback, useContext, useEffect, useState,
} from 'react';
import qs from 'qs';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../Config';
import { PaginatedDocs } from '../../../../mongoose/types';
import { ContextType, Props, Version } from './types';
@@ -21,6 +22,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
}) => {
const { serverURL, routes: { api } } = useConfig();
const { getPreference } = usePreferences();
const { i18n } = useTranslation();
const [publishedDoc, setPublishedDoc] = useState<TypeWithID & TypeWithTimestamps>(null);
const [versions, setVersions] = useState<PaginatedDocs<Version>>(null);
const [unpublishedVersions, setUnpublishedVersions] = useState<PaginatedDocs<Version>>(null);
@@ -112,14 +114,24 @@ export const DocumentInfoProvider: React.FC<Props> = ({
}
if (shouldFetch) {
let publishedJSON = await fetch(publishedFetchURL, { credentials: 'include' }).then((res) => res.json());
let publishedJSON = await fetch(publishedFetchURL, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
}).then((res) => res.json());
if (collection) {
publishedJSON = publishedJSON?.docs?.[0];
}
if (shouldFetchVersions) {
versionJSON = await fetch(`${versionFetchURL}?${qs.stringify(versionParams)}`, { credentials: 'include' }).then((res) => res.json());
versionJSON = await fetch(`${versionFetchURL}?${qs.stringify(versionParams)}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
}).then((res) => res.json());
if (publishedJSON?.updatedAt) {
const newerVersionParams = {
@@ -138,7 +150,12 @@ export const DocumentInfoProvider: React.FC<Props> = ({
};
// Get any newer versions available
const newerVersionRes = await fetch(`${versionFetchURL}?${qs.stringify(newerVersionParams)}`, { credentials: 'include' });
const newerVersionRes = await fetch(`${versionFetchURL}?${qs.stringify(newerVersionParams)}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
});
if (newerVersionRes.status === 200) {
unpublishedVersionJSON = await newerVersionRes.json();
@@ -150,7 +167,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
setVersions(versionJSON);
setUnpublishedVersions(unpublishedVersionJSON);
}
}, [global, collection, id, baseURL]);
}, [i18n, global, collection, id, baseURL]);
useEffect(() => {
getVersions();

View File

@@ -0,0 +1,24 @@
import React from 'react';
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import deepmerge from 'deepmerge';
import { defaultOptions } from '../../../../translations/defaultOptions';
import { useConfig } from '../Config';
export const I18n: React.FC = () => {
const config = useConfig();
if (i18n.isInitialized) {
return null;
}
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init(deepmerge(defaultOptions, config.i18n || { debug: true }));
return null;
};
export default I18n;

View File

@@ -1,5 +1,6 @@
import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../Config';
import { useAuth } from '../Auth';
import { requests } from '../../../api';
@@ -11,10 +12,11 @@ type PreferencesContext = {
const Context = createContext({} as PreferencesContext);
const requestOptions = (value) => ({
const requestOptions = (value, language) => ({
body: JSON.stringify({ value }),
headers: {
'Content-Type': 'application/json',
'Accept-Language': language,
},
});
@@ -23,6 +25,7 @@ export const PreferencesProvider: React.FC<{children?: React.ReactNode}> = ({ ch
const preferencesRef = useRef({});
const config = useConfig();
const { user } = useAuth();
const { i18n } = useTranslation();
const { serverURL, routes: { api } } = config;
useEffect(() => {
@@ -36,7 +39,11 @@ export const PreferencesProvider: React.FC<{children?: React.ReactNode}> = ({ ch
if (typeof preferencesRef.current[key] !== 'undefined') return preferencesRef.current[key];
const promise = new Promise((resolve: (value: T) => void) => {
(async () => {
const request = await requests.get(`${serverURL}${api}/_preferences/${key}`);
const request = await requests.get(`${serverURL}${api}/_preferences/${key}`, {
headers: {
'Accept-Language': i18n.language,
},
});
let value = null;
if (request.status === 200) {
const preference = await request.json();
@@ -48,12 +55,12 @@ export const PreferencesProvider: React.FC<{children?: React.ReactNode}> = ({ ch
});
preferencesRef.current[key] = promise;
return promise;
}, [api, preferencesRef, serverURL]);
}, [i18n.language, api, preferencesRef, serverURL]);
const setPreference = useCallback(async (key: string, value: unknown): Promise<void> => {
preferencesRef.current[key] = value;
await requests.post(`${serverURL}${api}/_preferences/${key}`, requestOptions(value));
}, [api, serverURL]);
await requests.post(`${serverURL}${api}/_preferences/${key}`, requestOptions(value, i18n.language));
}, [api, i18n.language, serverURL]);
contextRef.current.getPreference = getPreference;
contextRef.current.setPreference = setPreference;

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
import format from 'date-fns/format';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config';
import Eyebrow from '../../elements/Eyebrow';
import Form from '../../forms/Form';
@@ -18,6 +19,9 @@ import { Props } from './types';
import { OperationContext } from '../../utilities/OperationProvider';
import { ToggleTheme } from './ToggleTheme';
import { Gutter } from '../../elements/Gutter';
import ReactSelect from '../../elements/ReactSelect';
import Label from '../../forms/Label';
import type { Translation } from '../../../../translations/type';
import './index.scss';
@@ -47,6 +51,11 @@ const DefaultAccount: React.FC<Props> = (props) => {
} = collection;
const { admin: { dateFormat }, routes: { admin } } = useConfig();
const { t, i18n } = useTranslation('authentication');
const languageOptions = Object.entries(i18n.options.resources).map(([language, resource]) => (
{ label: (resource as Translation).general.thisLanguage, value: language }
));
const classes = [
baseClass,
@@ -68,9 +77,9 @@ const DefaultAccount: React.FC<Props> = (props) => {
>
<div className={`${baseClass}__main`}>
<Meta
title="Account"
description="Account of current user"
keywords="Account, Dashboard, Payload, CMS"
title={t('account')}
description={t('accountOfCurrentUser')}
keywords={t('account')}
/>
<Eyebrow />
{!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && (
@@ -79,7 +88,7 @@ const DefaultAccount: React.FC<Props> = (props) => {
<div className={`${baseClass}__edit`}>
<Gutter className={`${baseClass}__header`}>
<h1>
<RenderTitle {...{ data, useAsTitle, fallback: '[Untitled]' }} />
<RenderTitle {...{ data, useAsTitle, fallback: `[${t('general:untitled')}]` }} />
</h1>
<Auth
useAPIKey={auth.useAPIKey}
@@ -98,7 +107,17 @@ const DefaultAccount: React.FC<Props> = (props) => {
<Gutter
className={`${baseClass}__payload-settings`}
>
<h3>Payload Settings</h3>
<h3>{t('general:payloadSettings')}</h3>
<div className={`${baseClass}__language`}>
<Label
label={t('general:language')}
/>
<ReactSelect
value={languageOptions.find((language) => (language.value === i18n.language))}
options={languageOptions}
onChange={({ value }) => (i18n.changeLanguage(value))}
/>
</div>
<ToggleTheme />
</Gutter>
</div>
@@ -109,7 +128,7 @@ const DefaultAccount: React.FC<Props> = (props) => {
<ul className={`${baseClass}__collection-actions`}>
{(permissions?.create?.permission) && (
<React.Fragment>
<li><Link to={`${admin}/collections/${slug}/create`}>Create New</Link></li>
<li><Link to={`${admin}/collections/${slug}/create`}>{t('general:createNew')}</Link></li>
</React.Fragment>
)}
</ul>
@@ -119,7 +138,7 @@ const DefaultAccount: React.FC<Props> = (props) => {
data={data}
/>
{hasSavePermission && (
<FormSubmit>Save</FormSubmit>
<FormSubmit>{t('general:save')}</FormSubmit>
)}
</div>
<div className={`${baseClass}__sidebar-fields`}>
@@ -154,13 +173,13 @@ const DefaultAccount: React.FC<Props> = (props) => {
<React.Fragment>
{data.updatedAt && (
<li>
<div className={`${baseClass}__label`}>Last Modified</div>
<div className={`${baseClass}__label`}>{t('general:lastModified')}</div>
<div>{format(new Date(data.updatedAt), dateFormat)}</div>
</li>
)}
{data.createdAt && (
<li>
<div className={`${baseClass}__label`}>Created</div>
<div className={`${baseClass}__label`}>{t('general:created')}</div>
<div>{format(new Date(data.createdAt), dateFormat)}</div>
</li>
)}

View File

@@ -1,10 +1,12 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import RadioGroupInput from '../../../forms/field-types/RadioGroup/Input';
import { OnChange } from '../../../forms/field-types/RadioGroup/types';
import { Theme, useTheme } from '../../../utilities/Theme';
export const ToggleTheme: React.FC = () => {
const { theme, setTheme, autoMode } = useTheme();
const { t } = useTranslation('general');
const onChange = useCallback<OnChange<Theme>>((newTheme) => {
setTheme(newTheme);
@@ -13,20 +15,20 @@ export const ToggleTheme: React.FC = () => {
return (
<RadioGroupInput
name="theme"
label="Admin Theme"
label={t('adminTheme')}
value={autoMode ? 'auto' : theme}
onChange={onChange}
options={[
{
label: 'Automatic',
label: t('automatic'),
value: 'auto',
},
{
label: 'Light',
label: t('light'),
value: 'light',
},
{
label: 'Dark',
label: t('dark'),
value: 'dark',
},
]}

View File

@@ -150,6 +150,10 @@
border-top: 1px solid var(--theme-elevation-100);
}
&__language {
margin-bottom: $baseline;
}
@include mid-break {
&__main {
width: 100%;

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config';
import { useAuth } from '../../utilities/Auth';
import { useStepNav } from '../../elements/StepNav';
@@ -39,6 +40,7 @@ const AccountView: React.FC = () => {
user: 'users',
},
} = useConfig();
const { t } = useTranslation('authentication');
const collection = collections.find((coll) => coll.slug === adminUser);
@@ -59,21 +61,21 @@ const AccountView: React.FC = () => {
useEffect(() => {
const nav = [{
label: 'Account',
label: t('account'),
}];
setStepNav(nav);
}, [setStepNav]);
}, [setStepNav, t]);
useEffect(() => {
const awaitInitialState = async () => {
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, operation: 'update', id, user, locale });
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, operation: 'update', id, user, locale, t });
await getPreference(preferencesKey);
setInitialState(state);
};
awaitInitialState();
}, [dataToRender, fields, id, user, locale, preferencesKey, getPreference]);
}, [dataToRender, fields, id, user, locale, preferencesKey, getPreference, t]);
return (
<RenderCustomComponent

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config';
import { useAuth } from '../../utilities/Auth';
import MinimalTemplate from '../../templates/Minimal';
@@ -20,6 +21,7 @@ const CreateFirstUser: React.FC<Props> = (props) => {
const {
admin: { user: userSlug }, collections, serverURL, routes: { admin, api },
} = useConfig();
const { t } = useTranslation('authentication');
const userConfig = collections.find((collection) => collection.slug === userSlug);
@@ -34,17 +36,17 @@ const CreateFirstUser: React.FC<Props> = (props) => {
const fields = [
{
name: 'email',
label: 'Email Address',
label: t('general:emailAddress'),
type: 'email',
required: true,
}, {
name: 'password',
label: 'Password',
label: t('general:password'),
type: 'password',
required: true,
}, {
name: 'confirm-password',
label: 'Confirm Password',
label: t('confirmPassword'),
type: 'confirmPassword',
required: true,
},
@@ -52,12 +54,12 @@ const CreateFirstUser: React.FC<Props> = (props) => {
return (
<MinimalTemplate className={baseClass}>
<h1>Welcome</h1>
<p>To begin, create your first user.</p>
<h1>{t('general:welcome')}</h1>
<p>{t('beginCreateFirstUser')}</p>
<Meta
title="Create First User"
description="Create first user"
keywords="Create, Payload, CMS"
title={t('createFirstUser')}
description={t('createFirstUser')}
keywords={t('general:create')}
/>
<Form
onSuccess={onSuccess}
@@ -73,7 +75,7 @@ const CreateFirstUser: React.FC<Props> = (props) => {
]}
fieldTypes={fieldTypes}
/>
<FormSubmit>Create</FormSubmit>
<FormSubmit>{t('general:create')}</FormSubmit>
</Form>
</MinimalTemplate>
);

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config';
import Eyebrow from '../../elements/Eyebrow';
@@ -8,6 +9,7 @@ import Button from '../../elements/Button';
import { Props } from './types';
import { Gutter } from '../../elements/Gutter';
import { groupNavItems, Group, EntityToGroup, EntityType } from '../../../utilities/groupNavItems';
import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss';
@@ -21,6 +23,7 @@ const Dashboard: React.FC<Props> = (props) => {
} = props;
const { push } = useHistory();
const { i18n } = useTranslation('general');
const {
routes: {
@@ -54,8 +57,8 @@ const Dashboard: React.FC<Props> = (props) => {
return entityToGroup;
}),
], permissions));
}, [collections, globals, permissions]);
], permissions, i18n));
}, [collections, globals, i18n, permissions]);
return (
<div className={baseClass}>
@@ -74,14 +77,14 @@ const Dashboard: React.FC<Props> = (props) => {
let hasCreatePermission: boolean;
if (type === EntityType.collection) {
title = entity.labels.plural;
title = getTranslation(entity.labels.plural, i18n);
onClick = () => push({ pathname: `${admin}/collections/${entity.slug}` });
createHREF = `${admin}/collections/${entity.slug}/create`;
hasCreatePermission = permissions?.collections?.[entity.slug]?.create?.permission;
}
if (type === EntityType.global) {
title = entity.label;
title = getTranslation(entity.label, i18n);
onClick = () => push({ pathname: `${admin}/globals/${entity.slug}` });
}

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