feat: adds i18n functionality within Rest API, Local and Client contexts (#4749)

This commit is contained in:
Jarrod Flesch
2024-01-09 14:37:17 -05:00
committed by GitHub
parent f8d2f44f82
commit 56c766c7b8
306 changed files with 2462 additions and 1462 deletions

View File

@@ -1,11 +1,12 @@
import type { Create } from 'payload/database'
import type { Document, PayloadRequest } from 'payload/types'
import { ValidationError } from 'payload/errors'
import { i18nInit } from 'payload/utilities'
import type { MongooseAdapter } from '.'
import { withSession } from './withSession'
import { ValidationError } from 'payload/errors'
import { i18nInit } from 'payload/utilities'
export const create: Create = async function create(
this: MongooseAdapter,
@@ -26,7 +27,7 @@ export const create: Create = async function create(
message: req.t('error:valueMustBeUnique'),
},
],
req?.t ?? i18nInit(this.payload.config.i18n).t,
req.t,
)
: error
}

View File

@@ -2,7 +2,6 @@ import type { UpdateOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { ValidationError } from 'payload/errors'
import { i18nInit } from 'payload/utilities'
import type { MongooseAdapter } from '.'
@@ -40,7 +39,7 @@ export const updateOne: UpdateOne = async function updateOne(
message: 'Value must be unique',
},
],
req?.t ?? i18nInit(this.payload.config.i18n).t,
req.t,
)
: error
}

View File

@@ -2,4 +2,5 @@
/* DO NOT MODIFY it because it could be re-written at any time. */
export { GET, POST, DELETE, PATCH } from '@payloadcms/next/dist/routes/index'
// use the next line for easier local development
// export { GET, POST, DELETE, PATCH } from '@payloadcms/next/routes/index'

View File

@@ -23,7 +23,8 @@
"@payloadcms/next/layouts/*": ["../next/src/layouts/*"],
"@payloadcms/next/pages/*": ["../next/src/pages/*"],
"@payloadcms/next/routes/*": ["../next/src/routes/*"],
"@payloadcms/next/utilities/*": ["../next/src/utilities/*"]
"@payloadcms/next/utilities/*": ["../next/src/utilities/*"],
"@payloadcms/next/translations/*": ["../next/src/translations/*"]
}
},
"include": [

View File

@@ -40,6 +40,8 @@
"sass": "^1.69.5"
},
"dependencies": {
"@payloadcms/translations": "workspace:^",
"deepmerge": "^4.3.1",
"@faceless-ui/modal": "2.0.1",
"jsonwebtoken": "9.0.1",
"path-to-regexp": "^6.2.1",

View File

@@ -0,0 +1,7 @@
import type { Field, ClientConfig, SanitizedConfig } from 'payload/types'
export declare const sanitizeField: (f: any) => any
export declare const sanitizeFields: (fields: Field[]) => Field[]
export declare const createClientConfig: (
configPromise: SanitizedConfig | Promise<SanitizedConfig>,
) => Promise<ClientConfig>
//# sourceMappingURL=createClientConfig.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"createClientConfig.d.ts","sourceRoot":"","sources":["createClientConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEzE,eAAO,MAAM,aAAa,iBAmBzB,CAAA;AAED,eAAO,MAAM,cAAc,WAAY,KAAK,EAAE,KAAG,KAAK,EAA+B,CAAA;AAErF,eAAO,MAAM,kBAAkB,kBACd,eAAe,GAAG,QAAQ,eAAe,CAAC,KACxD,QAAQ,YAAY,CA6BtB,CAAA"}

View File

@@ -1,9 +1,9 @@
import React from 'react'
import { DefaultTemplate } from '@payloadcms/ui'
import { SanitizedConfig } from 'payload/types'
import { initPage } from '../../utilities/initPage'
import '@payloadcms/ui/scss/app.scss'
import { initPage } from '../../utilities/initPage'
export const metadata = {
title: 'Next.js',
@@ -17,10 +17,10 @@ export const AdminLayout = async ({
children: React.ReactNode
config: Promise<SanitizedConfig>
}) => {
const { user, permissions } = await initPage({ configPromise })
const { user, permissions, i18n } = await initPage({ configPromise })
return (
<DefaultTemplate config={configPromise} user={user} permissions={permissions}>
<DefaultTemplate config={configPromise} user={user} permissions={permissions} i18n={i18n}>
{children}
</DefaultTemplate>
)

View File

@@ -21,7 +21,7 @@ export const DocumentLayout = async ({
collectionSlug?: string
globalSlug?: string
}) => {
const { config, collectionConfig, globalConfig } = await initPage({
const { config, collectionConfig, globalConfig, i18n } = await initPage({
configPromise,
collectionSlug,
globalSlug,
@@ -30,6 +30,7 @@ export const DocumentLayout = async ({
return (
<Fragment>
<DocumentHeader
i18n={i18n}
config={config}
collectionConfig={collectionConfig}
globalConfig={globalConfig}

View File

@@ -1,7 +1,11 @@
import React from 'react'
import { headers, cookies } from 'next/headers'
import { translations } from '@payloadcms/translations/client'
import { RootProvider } from '@payloadcms/ui/providers'
import { SanitizedConfig } from 'payload/types'
import { deepMerge } from 'payload/utilities'
import { createClientConfig } from '../../createClientConfig'
import { getRequestLanguage } from '../../utilities/getRequestLanguage'
import '@payloadcms/ui/scss/app.scss'
@@ -10,6 +14,8 @@ export const metadata = {
description: 'Generated by Next.js',
}
const rtlLanguages = ['ar', 'fa', 'ha', 'ku', 'ur', 'ps', 'dv', 'ks', 'khw', 'he', 'yi']
export const RootLayout = async ({
children,
config: configPromise,
@@ -18,11 +24,32 @@ export const RootLayout = async ({
config: Promise<SanitizedConfig>
}) => {
const clientConfig = await createClientConfig(configPromise)
const lang =
getRequestLanguage({
headers: headers(),
cookies: cookies(),
}) ?? clientConfig.i18n.fallbackLanguage
const dir = rtlLanguages.includes(lang) ? 'RTL' : 'LTR'
const mergedTranslations = deepMerge(translations, clientConfig.i18n.translations)
const languageOptions = Object.entries(translations || {}).map(([language, translations]) => ({
label: translations.general.thisLanguage,
value: language,
}))
return (
<html lang="en" dir="LTR">
<html lang={lang} dir={dir}>
<body>
<RootProvider config={clientConfig}>{children}</RootProvider>
<RootProvider
config={clientConfig}
translations={mergedTranslations[lang]}
lang={lang}
fallbackLang={clientConfig.i18n.fallbackLanguage}
languageOptions={languageOptions}
>
{children}
</RootProvider>
</body>
</html>
)

View File

@@ -29,7 +29,7 @@ export const APIView = async ({
config: Promise<SanitizedConfig>
searchParams: { [key: string]: string | string[] | undefined }
}) => {
const { config, payload, user, locale, collectionConfig, globalConfig } = await initPage({
const { config, payload, user, locale, collectionConfig, globalConfig, i18n } = await initPage({
configPromise,
redirectUnauthenticatedUser: true,
collectionSlug,
@@ -140,8 +140,13 @@ export const APIView = async ({
>
<div className={`${baseClass}__form-fields`}>
<div className={`${baseClass}__filter-query-checkboxes`}>
{draftsEnabled && <Checkbox name="draft" path="draft" label="Draft" />}
<Checkbox name="authenticated" path="authenticated" label="Authenticated" />
{draftsEnabled && <Checkbox i18n={i18n} name="draft" path="draft" label="Draft" />}
<Checkbox
i18n={i18n}
name="authenticated"
path="authenticated"
label="Authenticated"
/>
</div>
{localeOptions && (
<Select label="Locale" name="locale" options={localeOptions} path="locale" />

View File

@@ -1,8 +1,6 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import type { Translation } from 'payload/dist/translations/type'
import { useTranslation } from '@payloadcms/ui/providers'
import { ReactSelect, Label } from '@payloadcms/ui'
import { ToggleTheme } from '../ToggleTheme'
@@ -15,14 +13,7 @@ export const Settings: React.FC<{
}> = (props) => {
const { className } = props
const { i18n, t } = useTranslation('authentication')
const languageOptions = Object.entries(i18n.options.resources || {}).map(
([language, resource]) => ({
label: (resource as Translation).general.thisLanguage,
value: language,
}),
)
const { i18n, t, languageOptions } = useTranslation()
return (
<div className={[baseClass, className].filter(Boolean).join(' ')}>
@@ -31,7 +22,8 @@ export const Settings: React.FC<{
<Label htmlFor="language-select" label={t('general:language')} />
<ReactSelect
inputId="language-select"
onChange={({ value }) => i18n.changeLanguage(value)}
// TODO(i18n): wire up onChange / changeLanguage fn
// onChange={({ value }) => i18n.changeLanguage(value)}
options={languageOptions}
value={languageOptions.find((language) => language.value === i18n.language)}
/>

View File

@@ -1,6 +1,6 @@
'use client'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '../../../providers/Translation'
import type { OnChange, Theme } from '@payloadcms/ui'
import { useTheme, RadioGroupInput } from '@payloadcms/ui'
@@ -11,7 +11,7 @@ export const ToggleTheme: React.FC = () => {
setTheme,
theme,
} = useTheme()
const { t } = useTranslation('general')
const { t } = useTranslation()
const onChange = useCallback<OnChange<Theme>>(
(newTheme) => {
@@ -22,20 +22,20 @@ export const ToggleTheme: React.FC = () => {
return (
<RadioGroupInput
label={t('adminTheme')}
label={t('general:adminTheme')}
name="theme"
onChange={onChange}
options={[
{
label: t('automatic'),
label: t('general:automatic'),
value: 'auto',
},
{
label: t('light'),
label: t('general:light'),
value: 'light',
},
{
label: t('dark'),
label: t('general:dark'),
value: 'dark',
},
]}

View File

@@ -34,6 +34,7 @@ export const DefaultAccount: React.FC<DefaultAccountViewProps> = (props) => {
onSave: onSaveFromProps,
permissions,
user,
i18n,
} = props
const { auth, fields } = collectionConfig
@@ -71,6 +72,7 @@ export const DefaultAccount: React.FC<DefaultAccountViewProps> = (props) => {
config={config}
collectionConfig={collectionConfig}
data={data}
i18n={i18n}
/>
<DocumentControls
apiURL={apiURL}
@@ -80,6 +82,7 @@ export const DefaultAccount: React.FC<DefaultAccountViewProps> = (props) => {
hasSavePermission={hasSavePermission}
isAccountView
permissions={permissions}
i18n={i18n}
/>
<DocumentFields
AfterFields={<Settings className={`${baseClass}__settings`} />}

View File

@@ -19,7 +19,7 @@ export const Account = async ({
config: Promise<SanitizedConfig>
searchParams: { [key: string]: string | string[] | undefined }
}) => {
const { config, payload, permissions, user } = await initPage({
const { config, payload, permissions, user, i18n } = await initPage({
configPromise,
redirectUnauthenticatedUser: true,
})
@@ -89,7 +89,7 @@ export const Account = async ({
locale,
operation: 'update',
preferences,
// t,
t: i18n.t,
user,
})
@@ -107,6 +107,7 @@ export const Account = async ({
user,
updatedAt: '', // TODO
id: user?.id,
i18n,
}
return (

View File

@@ -18,7 +18,7 @@ export const CollectionList = async ({
config: Promise<SanitizedConfig>
searchParams: { [key: string]: string | string[] | undefined }
}) => {
const { config, payload, permissions, user, collectionConfig } = await initPage({
const { config, payload, permissions, user, collectionConfig, i18n } = await initPage({
configPromise,
redirectUnauthenticatedUser: true,
collectionSlug,
@@ -63,6 +63,7 @@ export const CollectionList = async ({
setLimit: () => {},
setListControls: () => {},
setSort: () => {},
i18n,
}
return (

View File

@@ -1,27 +1,32 @@
import React from 'react'
import type { Field } from 'payload/types'
import { getNextT } from '../../utilities/getNextT'
import { MinimalTemplate, FormSubmit, Form, RenderFields, fieldTypes } from '@payloadcms/ui'
import './index.scss'
import { SanitizedConfig } from 'payload/types'
import i18n from 'i18next'
import { Metadata } from 'next'
import { meta } from '../../utilities/meta'
import './index.scss'
const baseClass = 'create-first-user'
export const generateMetadata = async ({
config,
}: {
config: Promise<SanitizedConfig>
}): Promise<Metadata> =>
meta({
title: i18n.t('createFirstUser'),
description: i18n.t('createFirstUser'),
keywords: i18n.t('general:create'),
}): Promise<Metadata> => {
const t = getNextT({
config: await config,
})
return meta({
title: t('createFirstUser'),
description: t('createFirstUser'),
keywords: t('general:create'),
config,
})
}
export const CreateFirstUser: React.FC<{
config: Promise<SanitizedConfig>

View File

@@ -1,6 +1,5 @@
'use client'
import React, { Fragment, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { EntityToGroup, Group } from '@payloadcms/ui'
import {
@@ -11,9 +10,10 @@ import {
useAuth,
useConfig,
useActions,
useTranslation,
} from '@payloadcms/ui'
import { getTranslation } from '@payloadcms/translations'
import { getTranslation } from 'payload/utilities'
import './index.scss'
const baseClass = 'dashboard'
@@ -37,7 +37,7 @@ export const DefaultDashboardClient: React.FC<{
const { permissions, user } = useAuth()
const { i18n, t } = useTranslation('general')
const { i18n, t } = useTranslation()
const [groups, setGroups] = useState<Group[]>([])
@@ -84,7 +84,7 @@ export const DefaultDashboardClient: React.FC<{
i18n,
),
)
}, [collectionsConfig, globalsConfig, i18n, permissions, user])
}, [collectionsConfig, globalsConfig, permissions, user])
return (
<Fragment>

View File

@@ -7,10 +7,8 @@ import { DefaultDashboard } from './Default'
export const Dashboard = async ({
config: configPromise,
searchParams,
}: {
config: Promise<SanitizedConfig>
searchParams: { [key: string]: string | string[] | undefined }
}) => {
const { config, user, permissions } = await initPage({
configPromise,

View File

@@ -31,6 +31,7 @@ export const DefaultGlobalView: React.FC<DefaultGlobalViewProps> = (props) => {
fieldTypes,
config,
user,
i18n,
} = props
// const { setViewActions } = useActions()
@@ -123,6 +124,7 @@ export const DefaultGlobalView: React.FC<DefaultGlobalViewProps> = (props) => {
hasSavePermission={hasSavePermission}
isEditing
permissions={permissions}
i18n={i18n}
/>
<DocumentFields
description={description}

View File

@@ -18,7 +18,7 @@ import { meta } from '../../utilities/meta'
import { DefaultGlobalView } from './Default'
import { DefaultGlobalViewProps } from './Default/types'
// import i18n from 'i18next'
// import { getTranslation } from 'payload/utilities'
// import { getTranslation } from '@payloadcms/translations'
export const generateMetadata = async ({
config,

View File

@@ -2,13 +2,13 @@ import React, { Fragment } from 'react'
import { Logo } from '@payloadcms/ui/graphics'
import { MinimalTemplate, LoginForm } from '@payloadcms/ui'
import './index.scss'
import type { SanitizedConfig } from 'payload/types'
import i18n from 'i18next'
import { meta } from '../../utilities/meta'
import { Metadata } from 'next'
import { initPage } from '../../utilities/initPage'
import { redirect } from 'next/navigation'
import { getNextT } from '../../utilities/getNextT'
import './index.scss'
const baseClass = 'login'
@@ -16,13 +16,18 @@ export const generateMetadata = async ({
config,
}: {
config: Promise<SanitizedConfig>
}): Promise<Metadata> =>
meta({
title: i18n.t('login'),
description: `${i18n.t('login')}`,
keywords: `${i18n.t('login')}`,
}): Promise<Metadata> => {
const t = getNextT({
config: await config,
})
return meta({
title: t('authentication:login'),
description: `${t('authentication:login')}`,
keywords: `${t('authentication:login')}`,
config,
})
}
export const Login: React.FC<{
config: Promise<SanitizedConfig>
@@ -49,9 +54,7 @@ export const Login: React.FC<{
<Logo config={config} />
</div>
{Array.isArray(beforeLogin) && beforeLogin.map((Component, i) => <Component key={i} />)}
{!collectionConfig?.auth?.disableLocalStrategy && (
<LoginForm config={config} searchParams={searchParams} />
)}
{!collectionConfig?.auth?.disableLocalStrategy && <LoginForm searchParams={searchParams} />}
{Array.isArray(afterLogin) && afterLogin.map((Component, i) => <Component key={i} />)}
</Fragment>
</MinimalTemplate>

View File

@@ -26,6 +26,7 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
id,
versionID,
locale,
i18n,
}) => {
const {
routes: { admin },
@@ -88,10 +89,12 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
url: `${admin}/globals/${globalConfig.slug}`,
},
{
// TODO(i18n)
label: 'Versions',
url: `${admin}/globals/${globalConfig.slug}/versions`,
},
{
// TODO(i18n)
label: '[Date]',
// label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n?.language) : '',
},
@@ -110,6 +113,7 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
const formattedCreatedAt = doc?.createdAt
// const formattedCreatedAt = doc?.createdAt
// TODO(i18n)
// ? formatDate(doc.createdAt, dateFormat, i18n?.language)
// : ''

View File

@@ -1,3 +1,4 @@
import { I18n } from '@payloadcms/translations'
import {
CollectionPermission,
FieldPermissions,
@@ -36,4 +37,5 @@ export type DefaultVersionsViewProps = {
versionID?: string
docPermissions: CollectionPermission | GlobalPermission
locale: string
i18n: I18n
}

View File

@@ -1,3 +1,4 @@
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
import type { ArrayField, BlockField, Field } from 'payload/types'
@@ -5,7 +6,7 @@ import type { Props } from '../types'
import RenderFieldsToDiff from '../..'
import { fieldAffectsData } from 'payload/types'
import { getTranslation, getUniqueListBy } from 'payload/utilities'
import { getUniqueListBy } from 'payload/utilities'
import Label from '../../Label'
import './index.scss'
@@ -80,7 +81,8 @@ const Iterable: React.FC<Props & { field: ArrayField | BlockField }> = ({
fieldComponents={fieldComponents}
fieldPermissions={permissions}
fields={subFields.filter(
(subField) => !(fieldAffectsData(subField) && subField.name === 'id'),
(subField) =>
!(fieldAffectsData(subField) && 'name' in subField && subField.name === 'id'),
)}
locales={locales}
version={versionRow}

View File

@@ -1,10 +1,10 @@
import React from 'react'
import { getTranslation } from '@payloadcms/translations'
import type { FieldWithSubFields } from 'payload/types'
import type { Props } from '../types'
import RenderFieldsToDiff from '../..'
import { getTranslation } from 'payload/utilities'
import Label from '../../Label'
import './index.scss'
@@ -28,6 +28,7 @@ const Nested: React.FC<Props & { field: FieldWithSubFields }> = ({
<Label>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{field.label}
// TODO(i18n)
{/* {getTranslation(field.label, i18n)} */}
</Label>
)}

View File

@@ -1,5 +1,6 @@
'use client'
import React from 'react'
import { getTranslation } from '@payloadcms/translations'
import ReactDiffViewer from 'react-diff-viewer-continued'
import { useTranslation } from 'react-i18next'
@@ -7,12 +8,12 @@ import type { RelationshipField, SanitizedCollectionConfig } from 'payload/types
import type { Props } from '../types'
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/types'
import { getTranslation } from 'payload/utilities'
import Label from '../../Label'
import { diffStyles } from '../styles'
import './index.scss'
import { useConfig, useLocale } from '@payloadcms/ui'
import './index.scss'
const baseClass = 'relationship-diff'
type RelationshipValue = Record<string, any>
@@ -101,7 +102,8 @@ const Relationship: React.FC<Props & { field: RelationshipField }> = ({
<div className={baseClass}>
<Label>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{getTranslation(field.label, i18n)}
// TODO(i18n)
{/* {getTranslation(field.label, i18n)} */}
</Label>
<ReactDiffViewer
hideLineNumbers

View File

@@ -1,17 +1,17 @@
import type { i18n as Ii18n } from 'i18next'
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
import { DiffMethod } from 'react-diff-viewer-continued'
import type { OptionObject, SelectField } from 'payload/types'
import type { Props } from '../types'
import { getTranslation } from 'payload/utilities'
import Label from '../../Label'
import { diffStyles } from '../styles'
import './index.scss'
import { DiffViewer } from './DiffViewer'
import './index.scss'
const baseClass = 'select-diff'
const getOptionsToRender = (

View File

@@ -1,8 +1,8 @@
import React from 'react'
import { getTranslation } from '@payloadcms/translations'
import type { Props } from '../types'
import { getTranslation } from 'payload/utilities'
import Label from '../../Label'
import { diffStyles } from '../styles'
import './index.scss'
@@ -37,6 +37,7 @@ const Text: React.FC<Props> = ({
<Label>
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
{typeof field.label === 'string' ? field.label : '[field-label]' /* TODO */}
// TODO(i18n)
{/* {getTranslation(field.label, i18n)} */}
</Label>
<DiffViewer

View File

@@ -1,13 +1,12 @@
'use client'
import { Modal, useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
import React, { Fragment, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'react-toastify'
import type { Props } from './types'
import { Button, MinimalTemplate, Pill, useConfig } from '@payloadcms/ui'
import { getTranslation } from 'payload/utilities'
import { Button, MinimalTemplate, Pill, useConfig, useTranslation } from '@payloadcms/ui'
// import { requests } from '../../../../api'
import './index.scss'
@@ -29,9 +28,9 @@ const Restore: React.FC<Props> = ({
} = useConfig()
const { toggleModal } = useModal()
const [processing, setProcessing] = useState(false)
const { i18n, t } = useTranslation('version')
const { i18n, t } = useTranslation()
const restoreMessage = t('aboutToRestoreGlobal', {
const restoreMessage = t('version:aboutToRestoreGlobal', {
label: getTranslation(label, i18n),
versionDate,
})
@@ -65,7 +64,7 @@ const Restore: React.FC<Props> = ({
toast.success(json.message)
// history.push(redirectURL)
} else {
toast.error(t('problemRestoringVersion'))
toast.error(t('version:problemRestoringVersion'))
}
}, [fetchURL, redirectURL, t, i18n])
@@ -75,11 +74,11 @@ const Restore: React.FC<Props> = ({
className={[baseClass, className].filter(Boolean).join(' ')}
onClick={() => toggleModal(modalSlug)}
>
{t('restoreThisVersion')}
{t('version:restoreThisVersion')}
</Pill>
<Modal className={`${baseClass}__modal`} slug={modalSlug}>
<MinimalTemplate className={`${baseClass}__modal-template`}>
<h1>{t('confirmVersionRestoration')}</h1>
<h1>{t('version:confirmVersionRestoration')}</h1>
<p>{restoreMessage}</p>
<Button
buttonStyle="secondary"
@@ -89,7 +88,7 @@ const Restore: React.FC<Props> = ({
{t('general:cancel')}
</Button>
<Button onClick={processing ? undefined : handleRestore}>
{processing ? t('restoring') : t('general:confirm')}
{processing ? t('version:restoring') : t('general:confirm')}
</Button>
</MinimalTemplate>
</Modal>

View File

@@ -27,12 +27,13 @@ export const VersionView = async ({
config: Promise<SanitizedConfig>
searchParams: { [key: string]: string | string[] | undefined }
}) => {
const { config, payload, permissions, user, collectionConfig, globalConfig } = await initPage({
configPromise,
redirectUnauthenticatedUser: true,
collectionSlug,
globalSlug,
})
const { config, payload, permissions, user, collectionConfig, globalConfig, i18n } =
await initPage({
configPromise,
redirectUnauthenticatedUser: true,
collectionSlug,
globalSlug,
})
const { localization } = config
@@ -162,6 +163,7 @@ export const VersionView = async ({
versionID={versionID}
docPermissions={docPermissions}
locale="" // TODO
i18n={i18n}
/>
)
}

View File

@@ -15,8 +15,17 @@ import './index.scss'
const baseClass = 'versions'
export const DefaultVersionsView: React.FC<DefaultVersionsViewProps> = (props) => {
const { id, config, collectionConfig, data, entityLabel, globalConfig, versionsData, limit } =
props
const {
id,
config,
collectionConfig,
data,
entityLabel,
globalConfig,
versionsData,
limit,
i18n,
} = props
// const useAsTitle = collectionConfig?.admin?.useAsTitle || 'id'
@@ -42,30 +51,35 @@ export const DefaultVersionsView: React.FC<DefaultVersionsViewProps> = (props) =
globalSlug={globalConfig?.slug}
id={id}
isEditing
view="Versions" // TODO; i18n
view={i18n.t('general:versions')}
pluralLabel={collectionConfig?.labels?.plural}
// view={t('versions')}
/>
{/* <LoadingOverlayToggle name="versions" show={isLoadingVersions} /> */}
<main className={baseClass}>
{/* <Meta description={metaDesc} title={metaTitle} /> */}
<Gutter className={`${baseClass}__wrap`}>
{versionCount === 0 && (
<div className={`${baseClass}__no-versions`}>{/* {t('noFurtherVersionsFound')} */}</div>
<div className={`${baseClass}__no-versions`}>
{i18n.t('version:noFurtherVersionsFound')}
</div>
)}
{versionCount > 0 && (
<React.Fragment>
{/* <div className={`${baseClass}__version-count`}>
{t(versionCount === 1 ? 'versionCount_one' : 'versionCount_many', {
count: versionCount,
})}
</div> */}
<div className={`${baseClass}__version-count`}>
{i18n.t(
versionCount === 1 ? 'version:versionCount_one' : 'version:versionCount_many',
{
count: versionCount,
},
)}
</div>
<Table
columns={buildVersionColumns({
config,
collectionConfig,
globalConfig,
docID: id,
t: i18n.t,
})}
data={versionsData?.docs}
/>
@@ -87,7 +101,7 @@ export const DefaultVersionsView: React.FC<DefaultVersionsViewProps> = (props) =
{versionsData.totalPages > 1 && versionsData.totalPages !== versionsData.page
? versionsData.limit * versionsData.page
: versionsData.totalDocs}{' '}
{/* {t('of')} {versionsData.totalDocs} */}
{i18n.t('general:of')} {versionsData.totalDocs}
</div>
<PerPage
limit={limit ? Number(limit) : 10}

View File

@@ -6,6 +6,7 @@ import type {
import type { PaginatedDocs } from 'payload/database'
import type { Version } from '@payloadcms/ui'
import { User } from 'payload/auth'
import { I18n } from '@payloadcms/translations'
export type DefaultVersionsViewProps = {
canAccessAdmin: boolean
@@ -19,4 +20,5 @@ export type DefaultVersionsViewProps = {
id: string
user: User
limit: number
i18n: I18n
}

View File

@@ -1,5 +1,3 @@
// import type { TFunction } from 'react-i18next'
import React from 'react'
import type {
@@ -15,6 +13,7 @@ import {
SortColumn,
} from '@payloadcms/ui'
import Link from 'next/link'
import { I18n } from '@payloadcms/translations'
type CreatedAtCellProps = {
config: SanitizedConfig
@@ -23,6 +22,7 @@ type CreatedAtCellProps = {
date: string
versionID: string
docID: string
i18n: I18n
}
const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
@@ -32,6 +32,7 @@ const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
collectionConfig,
// date,
globalConfig,
i18n,
}) => {
const {
routes: { admin },
@@ -47,6 +48,7 @@ const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
return (
<Link href={to}>
Date here
{/* TODO(i18n) */}
{/* {date && formatDate(date, dateFormat, i18n?.language)} */}
</Link>
)
@@ -59,25 +61,21 @@ export const buildVersionColumns = ({
collectionConfig,
globalConfig,
docID,
i18n: { t },
i18n,
}: {
config: SanitizedConfig
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
docID?: string
// t: TFunction,
i18n: I18n
}): Column[] => [
{
name: '',
accessor: 'updatedAt',
active: true,
components: {
Heading: (
<SortColumn
label="Updated At"
// label={t('general:updatedAt')}
name="updatedAt"
/>
),
Heading: <SortColumn label={t('general:updatedAt')} name="updatedAt" />,
renderCell: (row, data) => (
<CreatedAtCell
config={config}
@@ -86,6 +84,7 @@ export const buildVersionColumns = ({
globalConfig={globalConfig}
versionID={row?.id}
docID={docID}
i18n={i18n}
/>
),
},
@@ -96,14 +95,7 @@ export const buildVersionColumns = ({
accessor: 'id',
active: true,
components: {
Heading: (
<SortColumn
disable
label="ID"
// label={t('versionID')}
name="id"
/>
),
Heading: <SortColumn disable label={t('version:versionID')} name="id" />,
renderCell: (row, data) => <TextCell>{data}</TextCell>,
},
label: '',
@@ -113,40 +105,25 @@ export const buildVersionColumns = ({
accessor: 'autosave',
active: true,
components: {
Heading: (
<SortColumn
disable
label="Type"
// label={t('type')}
name="autosave"
/>
),
Heading: <SortColumn disable label={t('version:type')} name="autosave" />,
renderCell: (row) => (
<TextCell>
{row?.autosave && (
<React.Fragment>
<Pill>
Autosave
{/* {t('autosave')} */}
{t('version:autosave')}
</Pill>
&nbsp;&nbsp;
</React.Fragment>
)}
{row?.version._status === 'published' && (
<React.Fragment>
<Pill pillStyle="success">
Published
{/* {t('published')} */}
</Pill>
<Pill pillStyle="success">{t('version:published')}</Pill>
&nbsp;&nbsp;
</React.Fragment>
)}
{row?.version._status === 'draft' && (
<Pill>
Draft
{/* {t('draft')} */}
</Pill>
)}
{row?.version._status === 'draft' && <Pill>{t('version:draft')}</Pill>}
</TextCell>
),
},

View File

@@ -20,7 +20,7 @@ export const VersionsView = async ({
config: Promise<SanitizedConfig>
searchParams: { [key: string]: string | string[] | undefined }
}) => {
const { config, payload, permissions, user } = await initPage({
const { config, payload, permissions, user, i18n } = await initPage({
configPromise,
redirectUnauthenticatedUser: true,
collectionSlug,
@@ -193,6 +193,7 @@ export const VersionsView = async ({
user,
versionsData,
limit: limit ? parseInt(limit as string, 10) : undefined,
i18n,
}
return (

View File

@@ -16,7 +16,7 @@ export const me = async ({ req }: { req: PayloadRequest }): Promise<Response> =>
return Response.json(
{
...result,
message: 'Successfully retrieved me user.',
message: req.t('authentication:account'),
},
{
status: httpStatus.OK,

View File

@@ -28,6 +28,6 @@ export const create = async ({ req }: { req: PayloadRequest }): Promise<Response
// 'message',
// )
return Response.json(doc, {
status: httpStatus.OK,
status: httpStatus.CREATED,
})
}

View File

@@ -1,8 +1,8 @@
import httpStatus from 'http-status'
import type { PayloadRequest, Where } from 'payload/types'
import { isNumber } from 'payload/utilities'
import { getTranslation } from '@payloadcms/translations'
import { deleteOperation } from 'payload/operations'
// TODO(JARROD): pattern to catch errors and return correct Response
@@ -19,21 +19,23 @@ export const deleteDoc = async ({ req }: { req: PayloadRequest }): Promise<Respo
})
if (result.errors.length === 0) {
// const message = req.t('general:deletedCountSuccessfully', {
// count: result.docs.length,
// label: getTranslation(
// req.collection.config.labels[result.docs.length > 1 ? 'plural' : 'singular'],
// req.i18n,
// ),
// })
// res.status(httpStatus.OK).json({
// ...formatSuccessResponse(message, 'message'),
// ...result,
// })
return Response.json(result, {
status: httpStatus.OK,
const message = req.t('general:deletedCountSuccessfully', {
count: result.docs.length,
label: getTranslation(
req.collection.config.labels[result.docs.length > 1 ? 'plural' : 'singular'],
req.i18n,
),
})
return Response.json(
{
...result,
message,
},
{
status: httpStatus.OK,
},
)
}
// const total = result.docs.length + result.errors.length

View File

@@ -1,7 +1,6 @@
import httpStatus from 'http-status'
import type { PayloadRequest } from 'payload/types'
import { isNumber } from 'payload/utilities'
import { deleteByIDOperation } from 'payload/operations'
@@ -35,8 +34,8 @@ export const deleteByID = async ({
return Response.json(
{
...doc,
// ...formatSuccessResponse(req.t('general:successfullyDeleted'), 'message'),
doc,
message: req.t('general:deletedSuccessfully'),
},
{
status: httpStatus.OK,

View File

@@ -15,15 +15,26 @@ export const findByID = async ({
const { searchParams } = new URL(req.url)
const depth = searchParams.get('depth')
const result = await findByIDOperation({
id,
collection: req.collection,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
req,
})
try {
const result = await findByIDOperation({
id,
collection: req.collection,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
req,
})
return Response.json(result, {
status: httpStatus.OK,
})
return Response.json(result, {
status: httpStatus.OK,
})
} catch (error) {
return Response.json(
{
message: req.t('error:notFound'),
},
{
status: httpStatus.NOT_FOUND,
},
)
}
}

View File

@@ -124,6 +124,8 @@ const handleCustomEndpoints = ({
})
}
}
return null
}
export const GET = async (
@@ -141,12 +143,13 @@ export const GET = async (
})
if (req?.collection) {
await handleCustomEndpoints({
const customEndpointResponse = await handleCustomEndpoints({
entitySlug: slug1,
payloadRequest: req,
request,
endpoints: req.collection.config?.endpoints || [],
})
if (customEndpointResponse) return customEndpointResponse
switch (slug.length) {
case 1:
@@ -176,12 +179,13 @@ export const GET = async (
const globalConfig = req.payload.config.globals.find((global) => global.slug === slug2)
if (slug2) {
await handleCustomEndpoints({
const customEndpointResponse = await handleCustomEndpoints({
entitySlug: `${slug1}/${slug2}`,
payloadRequest: req,
request,
endpoints: globalConfig?.endpoints || [],
})
if (customEndpointResponse) return customEndpointResponse
}
switch (slug.length) {
@@ -200,15 +204,18 @@ export const GET = async (
}
} else {
// root routes
await handleCustomEndpoints({
const customEndpointResponse = await handleCustomEndpoints({
payloadRequest: req,
request,
endpoints: req.payload.config.endpoints,
})
if (customEndpointResponse) return customEndpointResponse
if (slug.length === 1 && slug1 === 'access') {
return endpoints.root.GET.access({ req })
}
return new Response('Route Not Found', { status: 404 })
}
}
@@ -221,12 +228,13 @@ export const POST = async (
const req = await createPayloadRequest({ request, config, params: { collection: slug1 } })
if (req?.collection) {
await handleCustomEndpoints({
const customEndpointResponse = await handleCustomEndpoints({
entitySlug: slug1,
payloadRequest: req,
request,
endpoints: req.collection.config?.endpoints || [],
})
if (customEndpointResponse) return customEndpointResponse
switch (slug.length) {
case 1:
@@ -256,12 +264,13 @@ export const POST = async (
const globalConfig = req.payload.config.globals.find((global) => global.slug === slug2)
if (slug2) {
await handleCustomEndpoints({
const customEndpointResponse = await handleCustomEndpoints({
entitySlug: `${slug1}/${slug2}`,
payloadRequest: req,
request,
endpoints: globalConfig?.endpoints || [],
})
if (customEndpointResponse) return customEndpointResponse
}
switch (slug.length) {
@@ -279,11 +288,12 @@ export const POST = async (
}
} else {
// root routes
await handleCustomEndpoints({
const customEndpointResponse = await handleCustomEndpoints({
payloadRequest: req,
request,
endpoints: req.payload.config.endpoints,
})
if (customEndpointResponse) return customEndpointResponse
}
}
@@ -302,12 +312,13 @@ export const DELETE = async (
})
if (req?.collection) {
await handleCustomEndpoints({
const customEndpointResponse = await handleCustomEndpoints({
entitySlug: slug1,
payloadRequest: req,
request,
endpoints: req.collection.config?.endpoints || [],
})
if (customEndpointResponse) return customEndpointResponse
switch (slug.length) {
case 1:
@@ -321,11 +332,12 @@ export const DELETE = async (
}
} else {
// root routes
await handleCustomEndpoints({
const customEndpointResponse = await handleCustomEndpoints({
payloadRequest: req,
request,
endpoints: req.payload.config.endpoints,
})
if (customEndpointResponse) return customEndpointResponse
}
}
@@ -344,12 +356,13 @@ export const PATCH = async (
})
if (req?.collection) {
await handleCustomEndpoints({
const customEndpointResponse = await handleCustomEndpoints({
entitySlug: slug1,
payloadRequest: req,
request,
endpoints: req.collection.config?.endpoints || [],
})
if (customEndpointResponse) return customEndpointResponse
switch (slug.length) {
case 1:
@@ -363,10 +376,11 @@ export const PATCH = async (
}
} else {
// root routes
await handleCustomEndpoints({
const customEndpointResponse = await handleCustomEndpoints({
payloadRequest: req,
request,
endpoints: req.payload.config.endpoints,
})
if (customEndpointResponse) return customEndpointResponse
}
}

View File

@@ -4,12 +4,21 @@ import { SanitizedConfig } from 'payload/types'
import { cache } from 'react'
export const auth = cache(
async ({ headers, config }: { headers: any; config: Promise<SanitizedConfig> }) => {
async ({
headers,
config,
cookies,
}: {
headers: any
config: Promise<SanitizedConfig>
cookies: Map<string, string>
}) => {
const payload = await getPayload({ config })
const user = await getAuthenticatedUser({
headers,
payload,
cookies,
})
const permissions = await getAccessResults({

View File

@@ -2,7 +2,9 @@ import type { SanitizedConfig, PayloadRequest, CustomPayloadRequest } from 'payl
import { getAuthenticatedUser } from 'payload/auth'
import { PayloadT, getPayload } from 'payload'
import { URL } from 'url'
import { i18nInit } from 'payload/utilities'
import { parseCookies } from './cookies'
import { getRequestLanguage } from './getRequestLanguage'
import { getNextI18n } from './getNextI18n'
type GetRequestLocalesArgs = {
localization: Exclude<PayloadT['config']['localization'], false>
@@ -90,7 +92,17 @@ export const createPayloadRequest = async ({
requestFallbackLocale = locales.fallbackLocale
}
const i18n = i18nInit(config.i18n)
const cookies = parseCookies(request.headers)
const language = getRequestLanguage({
headers: request.headers,
cookies,
})
const i18n = getNextI18n({
config,
language,
translationContext: 'api',
})
const customRequest: CustomPayloadRequest = {
payload,
@@ -119,6 +131,7 @@ export const createPayloadRequest = async ({
payload,
headers: req.headers,
isGraphQL,
cookies,
})
return req

View File

@@ -0,0 +1,20 @@
import { SanitizedConfig } from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { translations as clientTranslations } from '@payloadcms/translations/client'
import { translations as apiTranslations } from '@payloadcms/translations/api'
export const getNextI18n = ({
config,
language,
translationContext = 'client',
}: {
config: SanitizedConfig
language: string
translationContext?: 'api' | 'client'
}): ReturnType<typeof initI18n> => {
return initI18n({
config: config.i18n,
language,
translations: translationContext === 'api' ? apiTranslations : clientTranslations,
})
}

View File

@@ -0,0 +1,24 @@
import { translations } from '@payloadcms/translations/client'
import type { TFunction } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types'
import { initTFunction } from '@payloadcms/translations'
import { cookies, headers } from 'next/headers'
import { getRequestLanguage } from './getRequestLanguage'
export const getNextT = ({
config,
language,
}: {
config: SanitizedConfig
language?: string
}): TFunction => {
const lang = language || getRequestLanguage({ cookies: cookies(), headers: headers() })
return initTFunction({
language: lang,
config: config.i18n,
translations,
})
}

View File

@@ -0,0 +1,24 @@
import { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies'
import { matchLanguage } from '@payloadcms/translations'
type GetRequestLanguageArgs = {
cookies: Map<string, string> | ReadonlyRequestCookies
headers: Request['headers']
defaultLanguage?: string
}
export const getRequestLanguage = ({
headers,
cookies,
defaultLanguage = 'en',
}: GetRequestLanguageArgs): string => {
const acceptLanguage = headers.get('Accept-Language')
const cookieLanguage = cookies.get('lng')
let reqLanguage =
acceptLanguage ||
(typeof cookieLanguage === 'string' ? cookieLanguage : cookieLanguage.value) ||
defaultLanguage
return matchLanguage(reqLanguage)
}

View File

@@ -9,6 +9,9 @@ import type {
SanitizedGlobalConfig,
} from 'payload/types'
import { redirect } from 'next/navigation'
import { parseCookies } from './cookies'
import { getNextI18n } from './getNextI18n'
import { getRequestLanguage } from './getRequestLanguage'
import { findLocaleFromCode } from '../../../ui/src/utilities/findLocaleFromCode'
export const initPage = async ({
@@ -28,16 +31,20 @@ export const initPage = async ({
permissions: Awaited<ReturnType<typeof auth>>['permissions']
user: Awaited<ReturnType<typeof auth>>['user']
config: SanitizedConfig
i18n: ReturnType<typeof getNextI18n>
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
locale: ReturnType<typeof findLocaleFromCode>
}> => {
const headers = getHeaders()
const cookies = parseCookies(headers)
const { permissions, user } = await auth({
headers,
config: configPromise,
cookies,
})
const language = getRequestLanguage({ cookies, headers })
const config = await configPromise
@@ -52,6 +59,7 @@ export const initPage = async ({
config: configPromise,
})
const i18n = getNextI18n({ config, language })
let collectionConfig: SanitizedCollectionConfig
let globalConfig: SanitizedGlobalConfig
@@ -75,6 +83,7 @@ export const initPage = async ({
permissions,
user,
config,
i18n,
collectionConfig,
globalConfig,
locale,

View File

@@ -1,10 +1,9 @@
import { AuthStrategyFunctionArgs } from 'payload/auth'
import { parseCookies } from './cookies'
export const extractJWT = (
args: Pick<AuthStrategyFunctionArgs, 'headers' | 'payload'>,
args: Pick<AuthStrategyFunctionArgs, 'headers' | 'payload' | 'cookies'>,
): null | string => {
const { headers, payload } = args
const { headers, payload, cookies } = args
const jwtFromHeader = headers.get('Authorization')
const origin = headers.get('Origin')
@@ -18,7 +17,6 @@ export const extractJWT = (
return jwtFromHeader.replace('Bearer ', '')
}
const cookies = parseCookies(headers)
const tokenCookieName = `${payload.config.cookiePrefix}-token`
const cookieToken = cookies?.get(tokenCookieName)

View File

@@ -23,6 +23,7 @@
"src/**/*.spec.tsx"
],
"include": [
"src/translations/*.json",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.d.ts",

View File

@@ -59,6 +59,7 @@
"@faceless-ui/scroll-info": "1.3.0",
"@faceless-ui/window-info": "2.1.1",
"@monaco-editor/react": "4.5.1",
"@payloadcms/translations": "workspace:^",
"body-scroll-lock": "4.0.0-beta.0",
"bson-objectid": "2.0.4",
"conf": "10.2.0",
@@ -76,9 +77,6 @@
"graphql-scalars": "1.22.2",
"graphql-type-json": "0.3.2",
"http-status": "1.6.2",
"i18next": "22.5.1",
"i18next-browser-languagedetector": "6.1.8",
"i18next-http-middleware": "3.3.2",
"is-hotkey": "0.2.0",
"is-plain-object": "5.0.0",
"json-schema-to-typescript": "11.0.3",
@@ -101,7 +99,6 @@
"react-animate-height": "2.1.2",
"react-datepicker": "4.16.0",
"react-diff-viewer-continued": "3.2.6",
"react-i18next": "11.18.6",
"react-image-crop": "10.1.8",
"react-select": "5.7.4",
"react-toastify": "8.2.0",

View File

@@ -1,3 +1,4 @@
import { getTranslation } from '@payloadcms/translations'
import React, { Fragment, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
@@ -8,7 +9,6 @@ import type { SanitizedGlobalConfig } from '../../../../globals/config/types'
import type { FieldTypes } from '../../forms/field-types'
import type { EditViewProps } from '../types'
import { getTranslation } from '../../../../utilities/getTranslation'
import { DocumentControls } from '../../elements/DocumentControls'
import { DocumentFields } from '../../elements/DocumentFields'
import { LeaveWithoutSaving } from '../../modals/LeaveWithoutSaving'

View File

@@ -54,7 +54,6 @@ export const forgotPasswordOperation = async (incomingArgs: Arguments): Promise<
req: {
payload: { config, emailOptions, sendEmail: email },
payload,
t,
},
req,
} = args
@@ -103,11 +102,11 @@ export const forgotPasswordOperation = async (incomingArgs: Arguments): Promise<
? config.serverURL
: `${protocol}://${req.headers.get('host')}`
let html = `${t('authentication:youAreReceivingResetPassword')}
let html = `${req.t('authentication:youAreReceivingResetPassword')}
<a href="${serverURL}${config.routes.admin}/reset/${token}">
${serverURL}${config.routes.admin}/reset/${token}
</a>
${t('authentication:youDidNotRequestPassword')}`
${req.t('authentication:youDidNotRequestPassword')}`
if (typeof collectionConfig.auth.forgotPassword.generateEmailHTML === 'function') {
html = await collectionConfig.auth.forgotPassword.generateEmailHTML({
@@ -117,7 +116,7 @@ export const forgotPasswordOperation = async (incomingArgs: Arguments): Promise<
})
}
let subject = t('authentication:resetYourPassword')
let subject = req.t('authentication:resetYourPassword')
if (typeof collectionConfig.auth.forgotPassword.generateEmailSubject === 'function') {
subject = await collectionConfig.auth.forgotPassword.generateEmailSubject({

View File

@@ -4,7 +4,7 @@ import type { Result } from '../forgotPassword'
import { getDataLoader } from '../../../collections/dataloader'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { forgotPasswordOperation } from '../forgotPassword'
@@ -43,11 +43,13 @@ async function localForgotPassword<T extends keyof GeneratedTypes['collections']
)
}
const i18n = req?.i18n || getLocalI18n({ config: payload.config })
req.payloadAPI = req.payloadAPI || 'local'
req.payload = payload
req.i18n = i18nInit(payload.config.i18n)
req.i18n = i18n
req.t = i18n.t
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return forgotPasswordOperation({

View File

@@ -5,7 +5,7 @@ import type { Result } from '../login'
import { getDataLoader } from '../../../collections/dataloader'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { loginOperation } from '../login'
@@ -49,13 +49,15 @@ async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
)
}
const i18n = req?.i18n || getLocalI18n({ config: payload.config })
req.payloadAPI = req.payloadAPI || 'local'
req.payload = payload
req.i18n = i18nInit(payload.config.i18n)
req.i18n = i18n
req.t = i18n.t
req.locale = undefined
req.fallbackLocale = undefined
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
const args = {

View File

@@ -5,7 +5,7 @@ import type { Result } from '../resetPassword'
import { getDataLoader } from '../../../collections/dataloader'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { resetPasswordOperation } from '../resetPassword'
@@ -44,11 +44,13 @@ async function localResetPassword<T extends keyof GeneratedTypes['collections']>
)
}
const i18n = req?.i18n || getLocalI18n({ config: payload.config })
req.payload = payload
req.payloadAPI = req.payloadAPI || 'local'
req.i18n = i18nInit(payload.config.i18n)
req.i18n = i18n
req.t = i18n.t
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return resetPasswordOperation({

View File

@@ -4,7 +4,7 @@ import type { PayloadRequest } from '../../../types'
import { getDataLoader } from '../../../collections/dataloader'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { unlockOperation } from '../unlock'
@@ -39,11 +39,13 @@ async function localUnlock<T extends keyof GeneratedTypes['collections']>(
)
}
const i18n = req?.i18n || getLocalI18n({ config: payload.config })
req.payload = payload
req.payloadAPI = req.payloadAPI || 'local'
req.i18n = i18nInit(payload.config.i18n)
req.i18n = i18n
req.t = i18n.t
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return unlockOperation({

View File

@@ -3,7 +3,7 @@ import type { GeneratedTypes } from '../../../'
import type { PayloadRequest } from '../../../types'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { verifyEmailOperation } from '../verifyEmail'
@@ -29,9 +29,12 @@ async function localVerifyEmail<T extends keyof GeneratedTypes['collections']>(
)
}
const i18n = req?.i18n || getLocalI18n({ config: payload.config })
req.payload = payload
req.payloadAPI = req.payloadAPI || 'local'
req.i18n = i18nInit(payload.config.i18n)
req.i18n = i18n
req.t = i18n.t
return verifyEmailOperation({
collection,

View File

@@ -40,7 +40,6 @@ async function sendVerificationEmail(args: Args): Promise<void> {
const verificationURL = `${serverURL}${config.routes.admin}/${collectionConfig.slug}/verify/${token}`
let html = `${req.t('authentication:newAccountCreated', {
interpolation: { escapeValue: false },
serverURL: config.serverURL,
verificationURL,
})}`

View File

@@ -90,6 +90,7 @@ type GenerateForgotPasswordEmailSubject = (args?: {
}) => Promise<string> | string
export type AuthStrategyFunctionArgs = {
cookies?: Map<string, string>
headers: Request['headers']
isGraphQL?: boolean
payload: PayloadT

View File

@@ -209,7 +209,7 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
// /////////////////////////////////////
if (!collectionConfig.upload.disableLocalStorage) {
// await uploadFiles(payload, filesToUpload, req.t) // TODO: this was temporarily commented out bc it throws Sharp compilation errors in Next.js
// await uploadFiles(payload, filesToUpload, req) // TODO: this was temporarily commented out bc it throws Sharp compilation errors in Next.js
}
// /////////////////////////////////////

View File

@@ -66,7 +66,6 @@ export const deleteOperation = async <TSlug extends keyof GeneratedTypes['collec
locale,
payload: { config },
payload,
t,
},
req,
showHiddenFields,
@@ -139,7 +138,7 @@ export const deleteOperation = async <TSlug extends keyof GeneratedTypes['collec
config,
doc,
overrideDelete: true,
t,
req,
})
// /////////////////////////////////////

View File

@@ -57,7 +57,6 @@ export const deleteByIDOperation = async <TSlug extends keyof GeneratedTypes['co
req: {
payload: { config },
payload,
t,
},
req,
showHiddenFields,
@@ -101,15 +100,15 @@ export const deleteByIDOperation = async <TSlug extends keyof GeneratedTypes['co
where: combineQueries({ id: { equals: id } }, accessResults),
})
if (!docToDelete && !hasWhereAccess) throw new NotFound(t)
if (!docToDelete && hasWhereAccess) throw new Forbidden(t)
if (!docToDelete && !hasWhereAccess) throw new NotFound(req.t)
if (!docToDelete && hasWhereAccess) throw new Forbidden(req.t)
await deleteAssociatedFiles({
collectionConfig,
config,
doc: docToDelete,
overrideDelete: true,
t,
req,
})
// /////////////////////////////////////

View File

@@ -56,7 +56,7 @@ export const findByIDOperation = async <T extends TypeWithID>(
disableErrors,
draft: draftEnabled = false,
overrideAccess = false,
req: { locale, t },
req: { locale },
req,
showHiddenFields,
} = args
@@ -89,7 +89,7 @@ export const findByIDOperation = async <T extends TypeWithID>(
// Find by ID
// /////////////////////////////////////
if (!findOneArgs.where.and[0].id) throw new NotFound(t)
if (!findOneArgs.where.and[0].id) throw new NotFound(req.t)
if (!req.findByID) {
req.findByID = { [transactionID]: {} }
@@ -113,7 +113,7 @@ export const findByIDOperation = async <T extends TypeWithID>(
if (!result) {
if (!disableErrors) {
throw new NotFound(t)
throw new NotFound(req.t)
}
return null

View File

@@ -34,7 +34,7 @@ export const findVersionByIDOperation = async <T extends TypeWithID = any>(
depth,
disableErrors,
overrideAccess,
req: { locale, payload, t },
req: { locale, payload },
req,
showHiddenFields,
} = args
@@ -78,8 +78,8 @@ export const findVersionByIDOperation = async <T extends TypeWithID = any>(
if (!result) {
if (!disableErrors) {
if (!hasWhereAccess) throw new NotFound(t)
if (hasWhereAccess) throw new Forbidden(t)
if (!hasWhereAccess) throw new NotFound(req.t)
if (hasWhereAccess) throw new Forbidden(req.t)
}
return null

View File

@@ -8,7 +8,7 @@ import type { Document } from '../../../types'
import type { File } from '../../../uploads/types'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import getFileByPath from '../../../uploads/getFileByPath'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { getDataLoader } from '../../dataloader'
@@ -72,18 +72,20 @@ export default async function createLocal<TSlug extends keyof GeneratedTypes['co
)
}
const i18n = req?.i18n || getLocalI18n({ config: payload.config })
req.payloadAPI = req.payloadAPI || 'local'
req.locale = locale ?? req?.locale ?? defaultLocale
req.fallbackLocale = fallbackLocale !== 'undefined' ? fallbackLocale : defaultLocale
req.payload = payload
req.i18n = i18nInit(payload.config.i18n)
req.i18n = i18n
req.t = i18n.t
req.files = {
file: (file ?? (await getFileByPath(filePath))) as UploadedFile, // TODO(NATIVE_REQUEST): fix this type
}
if (typeof user !== 'undefined') req.user = user
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return createOperation<TSlug>({

View File

@@ -5,7 +5,7 @@ import type { Document, Where } from '../../../types'
import type { BulkOperationResult } from '../../config/types'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { getDataLoader } from '../../dataloader'
import { deleteOperation } from '../delete'
@@ -81,18 +81,20 @@ async function deleteLocal<TSlug extends keyof GeneratedTypes['collections']>(
)
}
const i18n = incomingReq?.i18n || getLocalI18n({ config: payload.config })
const req = {
fallbackLocale: typeof fallbackLocale !== 'undefined' ? fallbackLocale : defaultLocale,
i18n: i18nInit(payload.config.i18n),
i18n,
locale: locale ?? defaultLocale,
payload,
payloadAPI: 'local',
t: i18n.t,
transactionID: incomingReq?.transactionID,
user,
} as PayloadRequest
setRequestContext(req, context)
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
const args = {

View File

@@ -4,7 +4,7 @@ import type { PayloadRequest, RequestContext } from '../../../types'
import type { Document, Where } from '../../../types'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { getDataLoader } from '../../dataloader'
import { findOperation } from '../find'
@@ -78,13 +78,15 @@ export default async function findLocal<T extends keyof GeneratedTypes['collecti
fallbackLocaleToUse = fallbackLocale
}
const i18n = req?.i18n || getLocalI18n({ config: payload.config })
req.payloadAPI = req.payloadAPI || 'local'
req.locale = locale ?? req?.locale ?? defaultLocale
req.fallbackLocale = fallbackLocaleToUse
req.i18n = i18nInit(payload.config.i18n)
req.i18n = i18n
req.t = i18n.t
req.payload = payload
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
if (typeof user !== 'undefined') req.user = user

View File

@@ -4,7 +4,7 @@ import type { PayloadRequest, RequestContext } from '../../../types'
import type { Document } from '../../../types'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { getDataLoader } from '../../dataloader'
import { findByIDOperation } from '../findByID'
@@ -70,15 +70,17 @@ export default async function findByIDLocal<T extends keyof GeneratedTypes['coll
fallbackLocaleToUse = fallbackLocale
}
const i18n = req?.i18n || getLocalI18n({ config: payload.config })
req.payloadAPI = req.payloadAPI || 'local'
req.locale = locale ?? req?.locale ?? defaultLocale
req.fallbackLocale = fallbackLocaleToUse
req.i18n = i18nInit(payload.config.i18n)
req.i18n = i18n
req.t = i18n.t
req.payload = payload
if (typeof user !== 'undefined') req.user = user
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return findByIDOperation<GeneratedTypes['collections'][T]>({

View File

@@ -4,7 +4,7 @@ import type { Document } from '../../../types'
import type { TypeWithVersion } from '../../../versions/types'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { getDataLoader } from '../../dataloader'
import { findVersionByIDOperation } from '../findVersionByID'
@@ -68,13 +68,15 @@ export default async function findVersionByIDLocal<T extends keyof GeneratedType
fallbackLocaleToUse = fallbackLocale
}
const i18n = req?.i18n || getLocalI18n({ config: payload.config })
req.payloadAPI = req.payloadAPI || 'local'
req.locale = locale ?? req?.locale ?? defaultLocale
req.fallbackLocale = fallbackLocaleToUse
req.i18n = i18nInit(payload.config.i18n)
req.i18n = i18n
req.t = i18n.t
req.payload = payload
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return findVersionByIDOperation({

View File

@@ -5,7 +5,7 @@ import type { Document, Where } from '../../../types'
import type { TypeWithVersion } from '../../../versions/types'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { getDataLoader } from '../../dataloader'
import { findVersionsOperation } from '../findVersions'
@@ -61,19 +61,20 @@ export default async function findVersionsLocal<T extends keyof GeneratedTypes['
)
}
const i18n = i18nInit(payload.config.i18n)
const req = {
const i18n = incomingReq?.i18n || getLocalI18n({ config: payload.config })
const req: PayloadRequest = {
fallbackLocale: typeof fallbackLocale !== 'undefined' ? fallbackLocale : defaultLocale,
i18n,
locale: locale ?? defaultLocale,
payload,
payloadAPI: 'local',
t: i18n.t,
transactionID: incomingReq?.transactionID,
user,
} as PayloadRequest
setRequestContext(req, context)
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return findVersionsOperation({

View File

@@ -3,7 +3,7 @@ import type { PayloadRequest, RequestContext } from '../../../types'
import type { Document } from '../../../types'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { getDataLoader } from '../../dataloader'
import { restoreVersionOperation } from '../restoreVersion'
@@ -52,7 +52,8 @@ export default async function restoreVersionLocal<T extends keyof GeneratedTypes
)
}
const i18n = i18nInit(payload.config.i18n)
const i18n = incomingReq?.i18n || getLocalI18n({ config: payload.config })
const req = {
fallbackLocale,
i18n,

View File

@@ -7,7 +7,7 @@ import type { File } from '../../../uploads/types'
import type { BulkOperationResult } from '../../config/types'
import { APIError } from '../../../errors'
import { i18nInit } from '../../../translations/init'
import { getLocalI18n } from '../../../translations/getLocalI18n'
import getFileByPath from '../../../uploads/getFileByPath'
import { setRequestContext } from '../../../utilities/setRequestContext'
import { getDataLoader } from '../../dataloader'
@@ -86,7 +86,6 @@ async function updateLocal<TSlug extends keyof GeneratedTypes['collections']>(
} = options
const collection = payload.collections[collectionSlug]
const i18n = i18nInit(payload.config.i18n)
const defaultLocale = payload.config.localization
? payload.config.localization?.defaultLocale
: null
@@ -97,6 +96,8 @@ async function updateLocal<TSlug extends keyof GeneratedTypes['collections']>(
)
}
const i18n = incomingReq?.i18n || getLocalI18n({ config: payload.config })
const req = {
fallbackLocale: typeof fallbackLocale !== 'undefined' ? fallbackLocale : defaultLocale,
files: {
@@ -106,12 +107,12 @@ async function updateLocal<TSlug extends keyof GeneratedTypes['collections']>(
locale: locale ?? defaultLocale,
payload,
payloadAPI: 'local',
t: i18n.t,
transactionID: incomingReq?.transactionID,
user,
} as unknown as PayloadRequest
setRequestContext(req, context)
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
const args = {

View File

@@ -36,7 +36,7 @@ export const restoreVersionOperation = async <T extends TypeWithID = any>(
depth,
overrideAccess = false,
req,
req: { locale, payload, t },
req: { locale, payload },
showHiddenFields,
} = args
@@ -62,7 +62,7 @@ export const restoreVersionOperation = async <T extends TypeWithID = any>(
const [rawVersion] = versionDocs
if (!rawVersion) {
throw new NotFound(t)
throw new NotFound(req.t)
}
const parentDocID = rawVersion.parent
@@ -89,8 +89,8 @@ export const restoreVersionOperation = async <T extends TypeWithID = any>(
const doc = await req.payload.db.findOne(findOneArgs)
if (!doc && !hasWherePolicy) throw new NotFound(t)
if (!doc && hasWherePolicy) throw new Forbidden(t)
if (!doc && !hasWherePolicy) throw new NotFound(req.t)
if (!doc && hasWherePolicy) throw new Forbidden(req.t)
// /////////////////////////////////////
// fetch previousDoc

View File

@@ -74,7 +74,6 @@ export const updateOperation = async <TSlug extends keyof GeneratedTypes['collec
locale,
payload: { config },
payload,
t,
},
req,
showHiddenFields,
@@ -186,7 +185,7 @@ export const updateOperation = async <TSlug extends keyof GeneratedTypes['collec
// doc,
// // files: filesToUpload,
// overrideDelete: false,
// t,
// req,
// })
// /////////////////////////////////////
@@ -228,7 +227,7 @@ export const updateOperation = async <TSlug extends keyof GeneratedTypes['collec
// /////////////////////////////////////
// if (!collectionConfig.upload.disableLocalStorage) {
// await uploadFiles(payload, filesToUpload, t)
// await uploadFiles(payload, filesToUpload, req)
// }
// /////////////////////////////////////

View File

@@ -75,7 +75,6 @@ export const updateByIDOperation = async <TSlug extends keyof GeneratedTypes['co
locale,
payload: { config },
payload,
t,
},
req,
showHiddenFields,
@@ -121,8 +120,8 @@ export const updateByIDOperation = async <TSlug extends keyof GeneratedTypes['co
req,
})
if (!docWithLocales && !hasWherePolicy) throw new NotFound(t)
if (!docWithLocales && hasWherePolicy) throw new Forbidden(t)
if (!docWithLocales && !hasWherePolicy) throw new NotFound(req.t)
if (!docWithLocales && hasWherePolicy) throw new Forbidden(req.t)
const originalDoc = await afterRead({
collection: collectionConfig,
@@ -202,7 +201,7 @@ export const updateByIDOperation = async <TSlug extends keyof GeneratedTypes['co
// /////////////////////////////////////
// if (!collectionConfig.upload.disableLocalStorage) {
// await uploadFiles(payload, filesToUpload, t)
// await uploadFiles(payload, filesToUpload, req)
// }
// /////////////////////////////////////

View File

@@ -2,7 +2,7 @@ import path from 'path'
import type { Config } from './types'
export const defaults: Omit<Config, 'db' | 'editor'> = {
export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
admin: {
avatar: 'default',
buildPath: path.resolve(process.cwd(), './build'),

View File

@@ -1,3 +1,4 @@
import { translations } from '@payloadcms/translations/api'
import merge from 'deepmerge'
import { isPlainObject } from 'is-plain-object'
@@ -45,7 +46,7 @@ export const sanitizeConfig = (incomingConfig: Config): SanitizedConfig => {
isMergeableObject: isPlainObject,
}) as Config
if (!configWithDefaults.serverURL) {
if (!configWithDefaults?.serverURL) {
configWithDefaults.serverURL = ''
}
@@ -84,6 +85,13 @@ export const sanitizeConfig = (incomingConfig: Config): SanitizedConfig => {
}
}
config.i18n = {
fallbackLanguage: 'en',
supportedLanguages: Object.keys(translations),
translations,
...(incomingConfig?.i18n ?? {}),
}
configWithDefaults.collections.push(getPreferencesCollection(configWithDefaults))
configWithDefaults.collections.push(migrationsCollection)

View File

@@ -1,7 +1,7 @@
import type { I18nOptions } from '@payloadcms/translations'
import type { Express } from 'express'
import type { Options as ExpressFileUploadOptions } from 'express-fileupload'
import type GraphQL from 'graphql'
import type { InitOptions as i18nInitOptions } from 'i18next'
import type { Transporter } from 'nodemailer'
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
import type { DestinationStream, LoggerOptions } from 'pino'
@@ -614,20 +614,8 @@ export type Config = {
hooks?: {
afterError?: AfterErrorHook
}
/**
* Control the behaviour of the admin internationalisation.
*
* See i18next options.
*
* @default
* {
* fallbackLng: 'en',
* debug: false,
* supportedLngs: Object.keys(translations),
* resources: translations,
* }
*/
i18n?: i18nInitOptions
/** i18n config settings */
i18n?: I18nOptions
/** Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. */
indexSortableFields?: boolean
/**

View File

@@ -1,5 +1,6 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import httpStatus from 'http-status'
import APIError from './APIError'
@@ -7,7 +8,7 @@ import APIError from './APIError'
class AuthenticationError extends APIError {
constructor(t?: TFunction) {
super(
t ? t('error:emailOrPasswordIncorrect') : 'The email or password provided is incorrect.',
t ? t('error:emailOrPasswordIncorrect') : translations.en.error.emailOrPasswordIncorrect,
httpStatus.UNAUTHORIZED,
)
}

View File

@@ -1,5 +1,6 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import httpStatus from 'http-status'
import APIError from './APIError'
@@ -7,7 +8,7 @@ import APIError from './APIError'
class ErrorDeletingFile extends APIError {
constructor(t?: TFunction) {
super(
t ? t('error:deletingFile') : 'There was an error deleting file.',
t ? t('error:deletingFile') : translations.en.error.deletingFile,
httpStatus.INTERNAL_SERVER_ERROR,
)
}

View File

@@ -1,5 +1,6 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import httpStatus from 'http-status'
import APIError from './APIError'
@@ -7,7 +8,7 @@ import APIError from './APIError'
class FileUploadError extends APIError {
constructor(t?: TFunction) {
super(
t ? t('error:problemUploadingFile') : 'There was a problem while uploading the file.',
t ? t('error:problemUploadingFile') : translations.en.error.problemUploadingFile,
httpStatus.BAD_REQUEST,
)
}

View File

@@ -1,5 +1,6 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import httpStatus from 'http-status'
import APIError from './APIError'
@@ -7,7 +8,7 @@ import APIError from './APIError'
class Forbidden extends APIError {
constructor(t?: TFunction) {
super(
t ? t('error:notAllowedToPerformAction') : 'You are not allowed to perform this action.',
t ? t('error:notAllowedToPerformAction') : translations.en.error.notAllowedToPerformAction,
httpStatus.FORBIDDEN,
)
}

View File

@@ -1,17 +1,13 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import httpStatus from 'http-status'
import APIError from './APIError'
class LockedAuth extends APIError {
constructor(t?: TFunction) {
super(
t
? t('error:userLocked')
: 'This user is locked due to having too many failed login attempts.',
httpStatus.UNAUTHORIZED,
)
super(t ? t('error:userLocked') : translations.en.error.userLocked, httpStatus.UNAUTHORIZED)
}
}

View File

@@ -1,12 +1,16 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import httpStatus from 'http-status'
import APIError from './APIError'
class MissingFile extends APIError {
constructor(t?: TFunction) {
super(t ? t('error:noFilesUploaded') : 'No files were uploaded.', httpStatus.BAD_REQUEST)
super(
t ? t('error:noFilesUploaded') : translations.en.error.noFilesUploaded,
httpStatus.BAD_REQUEST,
)
}
}

View File

@@ -1,12 +1,13 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import httpStatus from 'http-status'
import APIError from './APIError'
class NotFound extends APIError {
constructor(t?: TFunction) {
super(t ? t('error:notFound') : 'The requested resource was not found.', httpStatus.NOT_FOUND)
super(t ? t('general:notFound') : translations.en.general.notFound, httpStatus.NOT_FOUND)
}
}

View File

@@ -1,14 +1,11 @@
import type { TFunction } from 'i18next'
import httpStatus from 'http-status'
import APIError from './APIError'
class QueryError extends APIError<{ path: string }[]> {
constructor(results: { path: string }[], t?: TFunction) {
const message = t
? t('error:unspecific', { count: results.length })
: `The following path${results.length === 1 ? '' : 's'} cannot be queried:`
constructor(results: { path: string }[]) {
const message = `The following path${results.length === 1 ? '' : 's'} cannot be queried:`
super(
`${message} ${results.map((err) => err.path).join(', ')}`,
httpStatus.BAD_REQUEST,

View File

@@ -1,15 +1,13 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import httpStatus from 'http-status'
import APIError from './APIError'
class UnauthorizedError extends APIError {
constructor(t?: TFunction) {
super(
t ? t('error:unauthorized') : 'Unauthorized, you must be logged in to make this request.',
httpStatus.UNAUTHORIZED,
)
super(t ? t('error:unauthorized') : translations.en.error.unauthorized, httpStatus.UNAUTHORIZED)
}
}

View File

@@ -1,5 +1,6 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import httpStatus from 'http-status'
import APIError from './APIError'
@@ -8,7 +9,10 @@ class ValidationError extends APIError<{ field: string; message: string }[]> {
constructor(results: { field: string; message: string }[], t?: TFunction) {
const message = t
? t('error:followingFieldsInvalid', { count: results.length })
: `The following field${results.length === 1 ? ' is' : 's are'} invalid:`
: results.length === 1
? translations.en.error.followingFieldsInvalid_one
: translations.en.error.followingFieldsInvalid_other
super(`${message} ${results.map((f) => f.field).join(', ')}`, httpStatus.BAD_REQUEST, results)
}
}

View File

@@ -1,4 +1,3 @@
export { default as errorHandler } from '../express/middleware/errorHandler'
export { default as APIError } from './APIError'
export { default as AuthenticationError } from './AuthenticationError'
export { default as DuplicateCollection } from './DuplicateCollection'

View File

@@ -1 +0,0 @@
export { defaultOptions } from '../translations/defaultOptions'

View File

@@ -3,7 +3,6 @@ export { promise as afterReadPromise } from '../fields/hooks/afterRead/promise'
export { traverseFields as afterReadTraverseFields } from '../fields/hooks/afterRead/traverseFields'
export { extractTranslations } from '../translations/extractTranslations'
export { i18nInit } from '../translations/init'
export { combineMerge } from '../utilities/combineMerge'
export {
@@ -22,10 +21,9 @@ export { formatLabels, formatNames, toWords } from '../utilities/formatLabels'
export { getIDType } from '../utilities/getIDType'
export { getObjectDotNotation } from '../utilities/getObjectDotNotation'
export { getTranslation } from '../utilities/getTranslation'
export { default as getUniqueListBy } from '../utilities/getUniqueListBy'
export { isNumber } from '../utilities/isNumber'
export { isValidID } from '../utilities/isValidID'
export { setsAreEqual } from '../utilities/setsAreEqual'

View File

@@ -1,13 +0,0 @@
import type { Payload } from '../payload'
async function initAdmin(ctx: Payload): Promise<void> {
if (!ctx.config.admin.disable) {
if (process.env.NODE_ENV === 'production') {
ctx.express.use(ctx.config.routes.admin, await ctx.config.admin.bundler.serve(ctx))
} else {
ctx.express.use(ctx.config.routes.admin, await ctx.config.admin.bundler.dev(ctx))
}
}
}
export default initAdmin

View File

@@ -1,34 +0,0 @@
import type { NextFunction, Request, Response } from 'express'
import passport from 'passport'
import type { SanitizedConfig } from '../../config/types'
export type PayloadAuthenticate = (req: Request, res: Response, next: NextFunction) => NextFunction
export default (config: SanitizedConfig): PayloadAuthenticate => {
const defaultMethods = ['jwt', 'anonymous']
const methods = config.collections.reduce((enabledMethods, collection) => {
if (typeof collection.auth === 'object') {
const collectionMethods = [...enabledMethods]
if (Array.isArray(collection.auth.strategies)) {
collection.auth.strategies.forEach(({ name, strategy }) => {
collectionMethods.unshift(`${collection.slug}-${name ?? strategy.name}`)
})
}
if (collection.auth.useAPIKey) {
collectionMethods.unshift(`${collection.slug}-api-key`)
}
return collectionMethods
}
return enabledMethods
}, defaultMethods)
const authenticate = passport.authenticate(methods, { session: false })
return authenticate
}

View File

@@ -1,16 +0,0 @@
import type { NextFunction, Request, Response } from 'express'
export default (req: Request, _: Response, next: NextFunction): void => {
if (req.body?._payload) {
const payloadJSON = JSON.parse(req.body._payload)
req.body = {
...req.body,
...payloadJSON,
}
delete req.body?._payload
}
next()
}

View File

@@ -1,22 +0,0 @@
import type { NextFunction, Request, Response } from 'express'
import type { SanitizedConfig } from '../../config/types'
export default (config: SanitizedConfig) => (req: Request, res: Response, next: NextFunction) => {
if (config.cors) {
res.header('Access-Control-Allow-Methods', 'PUT, PATCH, POST, GET, DELETE, OPTIONS')
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Content-Encoding, x-apollo-tracing',
)
if (config.cors === '*') {
res.setHeader('Access-Control-Allow-Origin', '*')
} else if (Array.isArray(config.cors) && config.cors.indexOf(req.headers.origin) > -1) {
res.header('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
}
}
next()
}

View File

@@ -1,12 +0,0 @@
import type { NextFunction, Response } from 'express'
import type { PayloadRequest } from '../types'
import { setRequestContext } from '../setRequestContext'
function defaultPayload(req: PayloadRequest, res: Response, next: NextFunction) {
setRequestContext(req)
next()
}
export default defaultPayload

View File

@@ -1,69 +0,0 @@
import type { NextFunction, Response } from 'express'
import type { Logger } from 'pino'
import httpStatus from 'http-status'
import type { SanitizedConfig } from '../../config/types'
import type { ErrorResponse } from '../responses/formatError'
import type { PayloadRequest } from '../types'
import APIError from '../../errors/APIError'
import formatErrorResponse from '../responses/formatError'
export type ErrorHandler = (
err: APIError,
req: PayloadRequest,
res: Response,
next: NextFunction,
) => Promise<Response<ErrorResponse> | void>
// NextFunction must be passed for Express to use this middleware as error handler
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const errorHandler =
(config: SanitizedConfig, logger: Logger) =>
async (
err: APIError,
req: PayloadRequest,
res: Response,
next: NextFunction,
): Promise<Response<ErrorResponse> | void> => {
let response = formatErrorResponse(err)
let status = err.status || httpStatus.INTERNAL_SERVER_ERROR
logger.error(err.stack)
// Internal server errors can contain anything, including potentially sensitive data.
// Therefore, error details will be hidden from the response unless `config.debug` is `true`
if (!config.debug && status === httpStatus.INTERNAL_SERVER_ERROR) {
response = formatErrorResponse(new APIError('Something went wrong.'))
}
if (config.debug && config.debug === true) {
response.stack = err.stack
}
if (req.collection && typeof req.collection.config.hooks.afterError === 'function') {
;({ response, status } = (await req.collection.config.hooks.afterError(
err,
response,
req.context,
req.collection.config,
)) || { response, status })
}
if (typeof config.hooks.afterError === 'function') {
;({ response, status } = (await config.hooks.afterError(
err,
response,
req.context,
req.collection.config,
)) || {
response,
status,
})
}
res.status(status).send(response)
}
export default errorHandler

View File

@@ -1,20 +0,0 @@
import type { Handler } from 'express'
import type { InitOptions } from 'i18next'
import deepmerge from 'deepmerge'
import i18next from 'i18next'
import i18nHTTPMiddleware from 'i18next-http-middleware'
import { defaultOptions } from '../../translations/defaultOptions'
const i18nMiddleware = (options: InitOptions): Handler => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
i18next.use(new i18nHTTPMiddleware.LanguageDetector(defaultOptions.detection)).init({
preload: defaultOptions.supportedLngs,
...deepmerge(defaultOptions, options || {}),
})
return i18nHTTPMiddleware.handle(i18next)
}
export { i18nMiddleware }

View File

@@ -1,8 +0,0 @@
const identifyAPI = (api) => {
return (req, _, next) => {
req.payloadAPI = api
next()
}
}
export default identifyAPI

View File

@@ -1,65 +0,0 @@
import bodyParser from 'body-parser'
import compression from 'compression'
import express from 'express'
import fileUpload from 'express-fileupload'
import rateLimit from 'express-rate-limit'
import methodOverride from 'method-override'
import passport from 'passport'
import qsMiddleware from 'qs-middleware'
import type { Payload } from '../../payload'
import type { PayloadRequest } from '../types'
import localizationMiddleware from '../../localization/middleware'
import authenticate from './authenticate'
import convertPayload from './convertPayload'
import corsHeaders from './corsHeaders'
import defaultPayload from './defaultPayload'
import { i18nMiddleware } from './i18n'
import identifyAPI from './identifyAPI'
const middleware = (payload: Payload): any => {
const rateLimitOptions: {
max?: number
skip?: (req: PayloadRequest) => boolean
windowMs?: number
} = {
max: payload.config.rateLimit.max,
windowMs: payload.config.rateLimit.window,
}
if (typeof payload.config.rateLimit.skip === 'function')
rateLimitOptions.skip = payload.config.rateLimit.skip
if (payload.config.express.middleware?.length) {
payload.logger.warn(
'express.middleware is deprecated. Please migrate to express.postMiddleware.',
)
}
return [
defaultPayload,
...(payload.config.express.preMiddleware || []),
rateLimit(rateLimitOptions),
passport.initialize(),
i18nMiddleware(payload.config.i18n),
identifyAPI('REST'),
methodOverride('X-HTTP-Method-Override'),
qsMiddleware({ arrayLimit: 1000, depth: 10 }),
bodyParser.urlencoded({ extended: true }),
compression(payload.config.express.compression),
localizationMiddleware(payload.config.localization),
express.json(payload.config.express.json),
fileUpload({
parseNested: true,
...payload.config.upload,
}),
convertPayload,
corsHeaders(payload.config),
authenticate(payload.config),
...(payload.config.express.middleware || []),
...(payload.config.express.postMiddleware || []),
]
}
export default middleware

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