Compare commits

..

8 Commits

Author SHA1 Message Date
Elliot DeNolf
d3131122db chore(release): v3.0.0-beta.67 [skip ci] 2024-07-18 14:00:49 -04:00
Alessio Gravili
6d0dfeafc8 chore: ensure fs operations in bundle scripts finish in sync (#7218)
Hopefully fixes broken releases
2024-07-18 13:44:26 -04:00
Patrik
00771b1f2a fix(ui): uploading from drawer & focal point positioning (#7117)
Fixes #7101
Fixes #7006

Drawers were sending duplicate query params. This new approach modeled after the fix in V2, ensures that each drawer has its own action url created per document and the query params will be created when that is generated.

Also fixes the following:
- incorrect focal point cropping
- generated filenames for animated image names used incorrect heights
2024-07-18 13:43:53 -04:00
Jarrod Flesch
448186f374 chore: use href for locale switching, warns user before leaving (#7215)
Opts to use links instead of router.replace when switching locales. The
main benefit is now the user will be warned if they have changes and
want to switch locales. Before it would switch locales and they would
lose any unsaved changes in the locale they came from.
2024-07-18 12:59:27 -04:00
Elliot DeNolf
0ada3df220 chore(release): v3.0.0-beta.66 [skip ci] 2024-07-18 12:25:49 -04:00
Jarrod Flesch
478fb8d3fd fix: cherry picks lockUntil fix from #6052 (#7213) 2024-07-18 12:14:31 -04:00
Jacob Fletcher
700baf1899 fix: aliases AfterMe, AfterLogout, and AfterRefresh hook types (#7216) 2024-07-18 15:49:50 +00:00
Jarrod Flesch
7b3b02198c feat: ability to login with email, username or both (#7086)
`auth.loginWithUsername`:

```ts
auth: {
  loginWithUsername: {
    allowEmailLogin: true, // default: false
    requireEmail: false, // default: false
  }
}
```

#### `allowEmailLogin`
This property will allow you to determine if users should be able to
login with either email or username. If set to `false`, the default
value, then users will only be able to login with usernames when using
the `loginWithUsername` property.

#### `requireEmail`
Require that users also provide emails when using usernames.
2024-07-18 10:29:44 -04:00
127 changed files with 2274 additions and 1623 deletions

View File

@@ -83,12 +83,50 @@ The following options are available:
| **`disableLocalStrategy`** | Advanced - disable Payload's built-in local auth strategy. Only use this property if you have replaced Payload's auth mechanisms with your own. |
| **`forgotPassword`** | Customize the way that the `forgotPassword` operation functions. [More details](./email#forgot-password). |
| **`lockTime`** | Set the time (in milliseconds) that a user should be locked out if they fail authentication more times than `maxLoginAttempts` allows for. |
| **`loginWithUsername`** | Ability to allow users to login with username/password. [More](/docs/authentication/overview#login-with-username) |
| **`maxLoginAttempts`** | Only allow a user to attempt logging in X amount of times. Automatically locks out a user from authenticating if this limit is passed. Set to `0` to disable. |
| **`strategies`** | Advanced - an array of custom authentification strategies to extend this collection's authentication with. [More details](./custom-strategies). |
| **`tokenExpiration`** | How long (in seconds) to keep the user logged in. JWTs and HTTP-only cookies will both expire at the same time. |
| **`useAPIKey`** | Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection. [More details](./api-keys). |
| **`verify`** | Set to `true` or pass an object with verification options to require users to verify by email before they are allowed to log into your app. [More details](./email#email-verification). |
### Login With Username
You can allow users to login with their username instead of their email address by setting the `loginWithUsername` property to `true`.
Example:
```ts
{
slug: 'customers',
auth: {
loginWithUsername: true,
},
}
```
Or, you can pass an object with additional options:
```ts
{
slug: 'customers',
auth: {
loginWithUsername: {
allowEmailLogin: true, // default: false
requireEmail: false, // default: false
},
},
}
```
**`allowEmailLogin`**
If set to `true`, users can log in with either their username or email address. If set to `false`, users can only log in with their username.
**`requireEmail`**
If set to `true`, an email address is required when creating a new user. If set to `false`, email is not required upon creation.
## Admin Auto-Login
For testing and demo purposes you may want to skip forcing the admin user to login in order to access the [Admin Panel](../admin/overview). Typically, all users should be required to login to access the Admin Panel, however, you can speed up local development time by enabling auto-login.

View File

@@ -180,7 +180,6 @@ import {
useFormInitializing,
useFormModified,
useFormProcessing,
useFormQueryParams,
useFormSubmitted,
useHotkey,
useIntersect,
@@ -221,7 +220,6 @@ import {
EntityVisibilityProvider,
FieldComponentsProvider,
FieldPropsProvider,
FormQueryParamsProvider,
ListInfoProvider,
ListQueryProvider,
LocaleProvider,

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -8,7 +8,7 @@ function unlockResolver(collection: Collection) {
async function resolver(_, args, context: Context) {
const options = {
collection,
data: { email: args.email },
data: { email: args.email, username: args.username },
req: isolateObjectProperty(context.req, 'transactionID'),
}

View File

@@ -429,12 +429,24 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
}
if (!collectionConfig.auth.disableLocalStrategy) {
const authArgs = {}
const canLoginWithEmail =
!collectionConfig.auth.loginWithUsername ||
collectionConfig.auth.loginWithUsername?.allowEmailLogin
const canLoginWithUsername = collectionConfig.auth.loginWithUsername
if (canLoginWithEmail) {
authArgs['email'] = { type: new GraphQLNonNull(GraphQLString) }
}
if (canLoginWithUsername) {
authArgs['username'] = { type: new GraphQLNonNull(GraphQLString) }
}
if (collectionConfig.auth.maxLoginAttempts > 0) {
graphqlResult.Mutation.fields[`unlock${singularName}`] = {
type: new GraphQLNonNull(GraphQLBoolean),
args: {
email: { type: new GraphQLNonNull(GraphQLString) },
},
args: authArgs,
resolve: unlock(collection),
}
}
@@ -455,9 +467,8 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
},
}),
args: {
email: { type: GraphQLString },
...authArgs,
password: { type: GraphQLString },
username: { type: GraphQLString },
},
resolve: login(collection),
}
@@ -466,8 +477,8 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
type: new GraphQLNonNull(GraphQLBoolean),
args: {
disableEmail: { type: GraphQLBoolean },
email: { type: new GraphQLNonNull(GraphQLString) },
expiration: { type: GraphQLInt },
...authArgs,
},
resolve: forgotPassword(collection),
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "The official live preview React SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -14,12 +14,12 @@ async function build() {
plugins: [sassPlugin({ css: 'external' })],
})
await fs.rename('dist/prod/esbuildEntry.css', 'dist/prod/styles.css', (err) => {
if (err) {
console.error(`Error while renaming index.css: ${err}`)
throw err
}
})
try {
fs.renameSync('dist/prod/esbuildEntry.css', 'dist/prod/styles.css')
} catch (err) {
console.error(`Error while renaming index.css: ${err}`)
throw err
}
console.log('styles.css bundled successfully')
@@ -32,12 +32,12 @@ async function build() {
]
for (const file of filesToDelete) {
await fs.unlink(file, (err) => {
if (err) {
console.error(`Error while deleting ${file}: ${err}`)
throw err
}
})
try {
fs.unlinkSync(file)
} catch (err) {
console.error(`Error while deleting ${file}: ${err}`)
throw err
}
}
console.log('Files renamed and deleted successfully')

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -8,9 +8,18 @@ import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const unlock: CollectionRouteHandler = async ({ collection, req }) => {
const { t } = req
const authData = collection.config.auth?.loginWithUsername
? {
email: typeof req.data?.email === 'string' ? req.data.email : '',
username: typeof req.data?.username === 'string' ? req.data.username : '',
}
: {
email: typeof req.data?.email === 'string' ? req.data.email : '',
}
await unlockOperation({
collection,
data: { email: req.data.email as string },
data: authData,
req,
})

View File

@@ -1,6 +1,6 @@
import type { AdminViewProps, ServerSideEditViewProps } from 'payload'
import { DocumentInfoProvider, FormQueryParamsProvider, HydrateClientUser } from '@payloadcms/ui'
import { DocumentInfoProvider, HydrateClientUser } from '@payloadcms/ui'
import { RenderCustomComponent } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js'
import React from 'react'
@@ -65,7 +65,6 @@ export const Account: React.FC<AdminViewProps> = async ({
return (
<DocumentInfoProvider
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} />}
action={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
collectionSlug={userSlug}
docPermissions={docPermissions}
@@ -84,31 +83,22 @@ export const Account: React.FC<AdminViewProps> = async ({
permissions={permissions}
/>
<HydrateClientUser permissions={permissions} user={user} />
<FormQueryParamsProvider
initialParams={{
depth: 0,
'fallback-locale': 'null',
locale: locale?.code,
uploadEdits: undefined,
<RenderCustomComponent
CustomComponent={
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined
}
DefaultComponent={EditView}
componentProps={viewComponentProps}
serverOnlyProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
>
<RenderCustomComponent
CustomComponent={
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined
}
DefaultComponent={EditView}
componentProps={viewComponentProps}
serverOnlyProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
</FormQueryParamsProvider>
/>
</DocumentInfoProvider>
)
}

View File

@@ -1,16 +1,6 @@
import type {
AdminViewComponent,
AdminViewProps,
EditViewComponent,
ServerSideEditViewProps,
} from 'payload'
import type { AdminViewComponent, AdminViewProps, EditViewComponent } from 'payload'
import {
DocumentInfoProvider,
EditDepthProvider,
FormQueryParamsProvider,
HydrateClientUser,
} from '@payloadcms/ui'
import { DocumentInfoProvider, EditDepthProvider, HydrateClientUser } from '@payloadcms/ui'
import { RenderCustomComponent, isEditing as getIsEditing } from '@payloadcms/ui/shared'
import { notFound, redirect } from 'next/navigation.js'
import React from 'react'
@@ -65,7 +55,6 @@ export const Document: React.FC<AdminViewProps> = async ({
let ErrorView: AdminViewComponent
let apiURL: string
let action: string
const { data, formState } = await getDocumentData({
id,
@@ -88,8 +77,6 @@ export const Document: React.FC<AdminViewProps> = async ({
notFound()
}
action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}`
const params = new URLSearchParams()
if (collectionConfig.versions?.drafts) {
params.append('draft', 'true')
@@ -128,8 +115,6 @@ export const Document: React.FC<AdminViewProps> = async ({
notFound()
}
action = `${serverURL}${apiRoute}/globals/${globalSlug}`
const params = new URLSearchParams({
locale: locale?.code,
})
@@ -198,7 +183,6 @@ export const Document: React.FC<AdminViewProps> = async ({
return (
<DocumentInfoProvider
action={action}
apiURL={apiURL}
collectionSlug={collectionConfig?.slug}
disableActions={false}
@@ -225,34 +209,25 @@ export const Document: React.FC<AdminViewProps> = async ({
depth={1}
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
>
<FormQueryParamsProvider
initialParams={{
depth: 0,
'fallback-locale': 'null',
locale: locale?.code,
uploadEdits: undefined,
}}
>
{ErrorView ? (
<ErrorView initPageResult={initPageResult} searchParams={searchParams} />
) : (
<RenderCustomComponent
CustomComponent={ViewOverride || CustomView}
DefaultComponent={DefaultView}
serverOnlyProps={{
i18n,
initPageResult,
locale,
params,
payload,
permissions,
routeSegments: segments,
searchParams,
user,
}}
/>
)}
</FormQueryParamsProvider>
{ErrorView ? (
<ErrorView initPageResult={initPageResult} searchParams={searchParams} />
) : (
<RenderCustomComponent
CustomComponent={ViewOverride || CustomView}
DefaultComponent={DefaultView}
serverOnlyProps={{
i18n,
initPageResult,
locale,
params,
payload,
permissions,
routeSegments: segments,
searchParams,
user,
}}
/>
)}
</EditDepthProvider>
</DocumentInfoProvider>
)

View File

@@ -98,14 +98,16 @@ export const Auth: React.FC<Props> = (props) => {
<div className={[baseClass, className].filter(Boolean).join(' ')}>
{!disableLocalStrategy && (
<React.Fragment>
{!loginWithUsername && (
{(!loginWithUsername ||
loginWithUsername?.allowEmailLogin ||
loginWithUsername?.requireEmail) && (
<EmailField
autoComplete="email"
disabled={disabled}
label={t('general:email')}
name="email"
readOnly={readOnly}
required
required={!loginWithUsername || loginWithUsername?.requireEmail}
/>
)}
{loginWithUsername && (

View File

@@ -5,7 +5,7 @@ export type Props = {
collectionSlug: SanitizedCollectionConfig['slug']
disableLocalStrategy?: boolean
email: string
loginWithUsername: boolean
loginWithUsername: SanitizedCollectionConfig['auth']['loginWithUsername']
operation: 'create' | 'update'
readOnly: boolean
requirePassword?: boolean

View File

@@ -13,7 +13,7 @@ import {
useDocumentEvents,
useDocumentInfo,
useEditDepth,
useFormQueryParams,
useUploadEdits,
} from '@payloadcms/ui'
import { getFormState } from '@payloadcms/ui/shared'
import { useRouter, useSearchParams } from 'next/navigation.js'
@@ -58,11 +58,13 @@ export const DefaultEditView: React.FC = () => {
const { refreshCookieAsync, user } = useAuth()
const config = useConfig()
const router = useRouter()
const { dispatchFormQueryParams } = useFormQueryParams()
const { getComponentMap, getFieldMap } = useComponentMap()
const params = useSearchParams()
const depth = useEditDepth()
const params = useSearchParams()
const { reportUpdate } = useDocumentEvents()
const { resetUploadEdits } = useUploadEdits()
const locale = params.get('locale')
const {
admin: { user: userSlug },
@@ -72,8 +74,6 @@ export const DefaultEditView: React.FC = () => {
serverURL,
} = config
const locale = params.get('locale')
const collectionConfig =
collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
@@ -130,12 +130,7 @@ export const DefaultEditView: React.FC = () => {
const redirectRoute = `${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`
router.push(redirectRoute)
} else {
dispatchFormQueryParams({
type: 'SET',
params: {
uploadEdits: null,
},
})
resetUploadEdits()
}
},
[
@@ -151,9 +146,9 @@ export const DefaultEditView: React.FC = () => {
isEditing,
refreshCookieAsync,
adminRoute,
locale,
router,
dispatchFormQueryParams,
locale,
resetUploadEdits,
],
)

View File

@@ -0,0 +1,109 @@
'use client'
import type { PayloadRequest } from 'payload'
import { EmailField, TextField, useConfig, useTranslation } from '@payloadcms/ui'
import { email, username } from 'payload/shared'
import React from 'react'
export type LoginFieldProps = {
type: 'email' | 'emailOrUsername' | 'username'
}
export const LoginField: React.FC<LoginFieldProps> = ({ type }) => {
const { t } = useTranslation()
const config = useConfig()
if (type === 'email') {
return (
<EmailField
autoComplete="email"
label={t('general:email')}
name="email"
required
validate={(value) =>
email(value, {
name: 'email',
type: 'email',
data: {},
preferences: { fields: {} },
req: { t } as PayloadRequest,
required: true,
siblingData: {},
})
}
/>
)
}
if (type === 'username') {
return (
<TextField
label={t('authentication:username')}
name="username"
required
validate={(value) =>
username(value, {
name: 'username',
type: 'text',
data: {},
preferences: { fields: {} },
req: {
payload: {
config,
},
t,
} as PayloadRequest,
required: true,
siblingData: {},
})
}
/>
)
}
if (type === 'emailOrUsername') {
return (
<TextField
label={t('authentication:emailOrUsername')}
name="username"
required
validate={(value) => {
const passesUsername = username(value, {
name: 'username',
type: 'text',
data: {},
preferences: { fields: {} },
req: {
payload: {
config,
},
t,
} as PayloadRequest,
required: true,
siblingData: {},
})
const passesEmail = email(value, {
name: 'username',
type: 'email',
data: {},
preferences: { fields: {} },
req: {
payload: {
config,
},
t,
} as PayloadRequest,
required: true,
siblingData: {},
})
if (!passesEmail && !passesUsername) {
return `${t('general:email')}: ${passesEmail} ${t('general:username')}: ${passesUsername}`
}
return true
}}
/>
)
}
return null
}

View File

@@ -8,17 +8,12 @@ const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.
import type { FormState, PayloadRequest } from 'payload'
import {
EmailField,
Form,
FormSubmit,
PasswordField,
TextField,
useConfig,
useTranslation,
} from '@payloadcms/ui'
import { email, password, text } from 'payload/shared'
import { Form, FormSubmit, PasswordField, useConfig, useTranslation } from '@payloadcms/ui'
import { password } from 'payload/shared'
import type { LoginFieldProps } from '../LoginField/index.js'
import { LoginField } from '../LoginField/index.js'
import './index.scss'
export const LoginForm: React.FC<{
@@ -36,7 +31,17 @@ export const LoginForm: React.FC<{
} = config
const collectionConfig = config.collections?.find((collection) => collection?.slug === userSlug)
const loginWithUsername = collectionConfig?.auth?.loginWithUsername
const { auth: authOptions } = collectionConfig
const loginWithUsername = authOptions.loginWithUsername
const canLoginWithEmail =
!authOptions.loginWithUsername || authOptions.loginWithUsername.allowEmailLogin
const canLoginWithUsername = authOptions.loginWithUsername
const [loginType] = React.useState<LoginFieldProps['type']>(() => {
if (canLoginWithEmail && canLoginWithUsername) return 'emailOrUsername'
if (canLoginWithUsername) return 'username'
return 'email'
})
const { t } = useTranslation()
@@ -75,47 +80,7 @@ export const LoginForm: React.FC<{
waitForAutocomplete
>
<div className={`${baseClass}__inputWrap`}>
{loginWithUsername ? (
<TextField
label={t('authentication:username')}
name="username"
required
validate={(value) =>
text(value, {
name: 'username',
type: 'text',
data: {},
preferences: { fields: {} },
req: {
payload: {
config,
},
t,
} as PayloadRequest,
required: true,
siblingData: {},
})
}
/>
) : (
<EmailField
autoComplete="email"
label={t('general:email')}
name="email"
required
validate={(value) =>
email(value, {
name: 'email',
type: 'email',
data: {},
preferences: { fields: {} },
req: { t } as PayloadRequest,
required: true,
siblingData: {},
})
}
/>
)}
<LoginField type={loginType} />
<PasswordField
autoComplete="off"
label={t('general:password')}

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",

View File

@@ -1,6 +1,6 @@
import type { Field } from '../../fields/config/types.js'
export default [
export const accountLockFields: Field[] = [
{
name: 'loginAttempts',
type: 'number',

View File

@@ -7,8 +7,7 @@ const encryptKey: FieldHook = ({ req, value }) =>
const decryptKey: FieldHook = ({ req, value }) =>
value ? req.payload.decrypt(value as string) : undefined
// eslint-disable-next-line no-restricted-exports
export default [
export const apiKeyFields = [
{
name: 'enableAPIKey',
type: 'checkbox',

View File

@@ -1,6 +1,6 @@
import type { Field } from '../../fields/config/types.js'
const baseAuthFields: Field[] = [
export const baseAuthFields: Field[] = [
{
name: 'resetPasswordToken',
type: 'text',
@@ -22,5 +22,3 @@ const baseAuthFields: Field[] = [
hidden: true,
},
]
export default baseAuthFields

View File

@@ -0,0 +1,26 @@
import type { Field } from '../../fields/config/types.js'
import { email } from '../../fields/validations.js'
export const emailField = ({ required = true }: { required?: boolean }): Field => ({
name: 'email',
type: 'email',
admin: {
components: {
Field: required ? () => null : undefined,
},
},
hooks: {
beforeChange: [
({ value }) => {
if (value) {
return value.toLowerCase().trim()
}
},
],
},
label: ({ t }) => t('general:email'),
required,
unique: true,
validate: email,
})

View File

@@ -1,38 +0,0 @@
import type { Field } from '../../fields/config/types.js'
import { email } from '../../fields/validations.js'
export default (field: string): Field[] => {
const formattedFields = [
{
name: 'email',
type: 'email',
admin: {
components: {
Field: field === 'username' ? undefined : () => null,
},
},
label: ({ t }) => t('general:email'),
required: true,
unique: field === 'email' ? true : false,
validate: email,
},
] as Field[]
if (field === 'username') {
formattedFields.push({
name: 'username',
type: 'text',
admin: {
components: {
Field: () => null,
},
},
label: ({ t }) => t('authentication:username'),
required: true,
unique: true,
} as Field)
}
return formattedFields
}

View File

@@ -0,0 +1,26 @@
import type { Field } from '../../fields/config/types.js'
import { username } from '../../fields/validations.js'
export const usernameField: Field = {
name: 'username',
type: 'text',
admin: {
components: {
Field: () => null,
},
},
hooks: {
beforeChange: [
({ value }) => {
if (value) {
return value.toLowerCase().trim()
}
},
],
},
label: ({ t }) => t('authentication:username'),
required: true,
unique: true,
validate: username,
}

View File

@@ -15,7 +15,7 @@ const autoRemoveVerificationToken: FieldHook = ({ data, operation, originalDoc,
return value
}
export default [
export const verificationFields: Field[] = [
{
name: '_verified',
type: 'checkbox',

View File

@@ -0,0 +1,44 @@
import type { Field } from '../fields/config/types.js'
import type { IncomingAuthType } from './types.js'
import { accountLockFields } from './baseFields/accountLock.js'
import { apiKeyFields } from './baseFields/apiKey.js'
import { baseAuthFields } from './baseFields/auth.js'
import { emailField } from './baseFields/email.js'
import { usernameField } from './baseFields/username.js'
import { verificationFields } from './baseFields/verification.js'
export const getBaseAuthFields = (authConfig: IncomingAuthType): Field[] => {
const authFields: Field[] = []
if (authConfig.useAPIKey) {
authFields.push(...apiKeyFields)
}
if (!authConfig.disableLocalStrategy) {
const emailFieldIndex = authFields.push(emailField({ required: true })) - 1
if (authConfig.loginWithUsername) {
if (
typeof authConfig.loginWithUsername === 'object' &&
authConfig.loginWithUsername.requireEmail === false
) {
authFields[emailFieldIndex] = emailField({ required: false })
}
authFields.push(usernameField)
}
authFields.push(...baseAuthFields)
if (authConfig.verify) {
authFields.push(...verificationFields)
}
if (authConfig.maxLoginAttempts > 0) {
authFields.push(...accountLockFields)
}
}
return authFields
}

View File

@@ -1,2 +1,5 @@
const isLocked = (date: number): boolean => !!(date && date > Date.now())
const isLocked = (date: number): boolean => {
if (!date) return false
return date > Date.now()
}
export default isLocked

View File

@@ -7,7 +7,7 @@ import type {
Collection,
} from '../../collections/config/types.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest } from '../../types/index.js'
import type { PayloadRequest, Where } from '../../types/index.js'
import { buildAfterOperation } from '../../collections/operations/utils.js'
import { APIError } from '../../errors/index.js'
@@ -30,9 +30,20 @@ export type Result = string
export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
incomingArgs: Arguments<TSlug>,
): Promise<null | string> => {
const loginWithUsername = incomingArgs.collection?.config?.auth?.loginWithUsername
const loginWithUsername = incomingArgs.collection.config.auth.loginWithUsername
const { data } = incomingArgs
if (!incomingArgs.data.email && !incomingArgs.data.username) {
const canLoginWithUsername = Boolean(loginWithUsername)
const canLoginWithEmail = !loginWithUsername || loginWithUsername.allowEmailLogin
const sanitizedEmail =
(canLoginWithEmail && (incomingArgs.data.email || '').toLowerCase().trim()) || null
const sanitizedUsername =
'username' in data && typeof data?.username === 'string'
? data.username.toLowerCase().trim()
: null
if (!sanitizedEmail && !sanitizedUsername) {
throw new APIError(
`Missing ${loginWithUsername ? 'username' : 'email'}.`,
httpStatus.BAD_REQUEST,
@@ -85,20 +96,33 @@ export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
resetPasswordToken?: string
}
if (!data.email && !data.username) {
if (!sanitizedEmail && !sanitizedUsername) {
throw new APIError(
`Missing ${loginWithUsername ? 'username' : 'email'}.`,
httpStatus.BAD_REQUEST,
)
}
let whereConstraint: Where = {}
if (canLoginWithEmail && sanitizedEmail) {
whereConstraint = {
email: {
equals: sanitizedEmail,
},
}
} else if (canLoginWithUsername && sanitizedUsername) {
whereConstraint = {
username: {
equals: sanitizedUsername,
},
}
}
let user = await payload.db.findOne<UserDoc>({
collection: collectionConfig.slug,
req,
where:
loginWithUsername && data?.username
? { username: { equals: data.username } }
: { email: { equals: data.email.toLowerCase() } },
where: whereConstraint,
})
// We don't want to indicate specifically that an email was not found,
@@ -116,7 +140,7 @@ export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
req,
})
if (!disableEmail) {
if (!disableEmail && user.email) {
const protocol = new URL(req.url).protocol // includes the final :
const serverURL =
config.serverURL !== null && config.serverURL !== ''
@@ -149,7 +173,7 @@ export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
from: `"${email.defaultFromName}" <${email.defaultFromAddress}>`,
html,
subject,
to: loginWithUsername ? user.email : data.email,
to: user.email,
})
}

View File

@@ -1,23 +1,26 @@
import type { CollectionSlug, Payload, RequestContext } from '../../../index.js'
import type {
AuthOperationsFromCollectionSlug,
CollectionSlug,
Payload,
RequestContext,
} from '../../../index.js'
import type { PayloadRequest } from '../../../types/index.js'
import { APIError } from '../../../errors/index.js'
import { createLocalReq } from '../../../utilities/createLocalReq.js'
import { unlockOperation } from '../unlock.js'
export type Options<T extends CollectionSlug> = {
collection: T
export type Options<TSlug extends CollectionSlug> = {
collection: TSlug
context?: RequestContext
data: {
email
}
data: AuthOperationsFromCollectionSlug<TSlug>['unlock']
overrideAccess: boolean
req?: PayloadRequest
}
async function localUnlock<T extends CollectionSlug>(
async function localUnlock<TSlug extends CollectionSlug>(
payload: Payload,
options: Options<T>,
options: Options<TSlug>,
): Promise<boolean> {
const { collection: collectionSlug, data, overrideAccess = true } = options
@@ -29,7 +32,7 @@ async function localUnlock<T extends CollectionSlug>(
)
}
return unlockOperation({
return unlockOperation<TSlug>({
collection,
data,
overrideAccess,

View File

@@ -6,7 +6,7 @@ import type {
DataFromCollectionSlug,
} from '../../collections/config/types.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest } from '../../types/index.js'
import type { PayloadRequest, Where } from '../../types/index.js'
import type { User } from '../types.js'
import { buildAfterOperation } from '../../collections/operations/utils.js'
@@ -82,26 +82,47 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// /////////////////////////////////////
let user
const loginWithUsername = collectionConfig?.auth?.loginWithUsername
const { email: unsanitizedEmail, password } = data
const username = 'username' in data && data.username
const loginWithUsername = collectionConfig.auth.loginWithUsername
if (loginWithUsername && !username) {
const sanitizedEmail =
typeof unsanitizedEmail === 'string' ? unsanitizedEmail.toLowerCase().trim() : null
const sanitizedUsername =
'username' in data && typeof data?.username === 'string'
? data.username.toLowerCase().trim()
: null
const canLoginWithUsername = Boolean(loginWithUsername)
const canLoginWithEmail = !loginWithUsername || loginWithUsername.allowEmailLogin
// cannot login with email, did not provide username
if (!canLoginWithEmail && !sanitizedUsername) {
throw new ValidationError({
collection: collectionConfig.slug,
errors: [{ field: 'username', message: req.i18n.t('validation:required') }],
})
}
if (
!loginWithUsername &&
(typeof unsanitizedEmail !== 'string' || unsanitizedEmail.trim() === '')
) {
// cannot login with username, did not provide email
if (!canLoginWithUsername && !sanitizedEmail) {
throw new ValidationError({
collection: collectionConfig.slug,
errors: [{ field: 'email', message: req.i18n.t('validation:required') }],
})
}
// can login with either email or username, did not provide either
if (!sanitizedUsername && !sanitizedEmail) {
throw new ValidationError({
collection: collectionConfig.slug,
errors: [
{ field: 'email', message: req.i18n.t('validation:required') },
{ field: 'username', message: req.i18n.t('validation:required') },
],
})
}
// did not provide password for login
if (typeof password !== 'string' || password.trim() === '') {
throw new ValidationError({
collection: collectionConfig.slug,
@@ -109,22 +130,59 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
})
}
const email = unsanitizedEmail ? unsanitizedEmail.toLowerCase().trim() : null
let whereConstraint: Where = {}
const emailConstraint: Where = {
email: {
equals: sanitizedEmail,
},
}
const usernameConstraint: Where = {
username: {
equals: sanitizedUsername,
},
}
if (canLoginWithEmail && canLoginWithUsername && (sanitizedUsername || sanitizedEmail)) {
if (sanitizedUsername) {
whereConstraint = {
or: [
usernameConstraint,
{
email: {
equals: sanitizedUsername,
},
},
],
}
} else {
whereConstraint = {
or: [
emailConstraint,
{
username: {
equals: sanitizedEmail,
},
},
],
}
}
} else if (canLoginWithEmail && sanitizedEmail) {
whereConstraint = emailConstraint
} else if (canLoginWithUsername && sanitizedUsername) {
whereConstraint = usernameConstraint
}
user = await payload.db.findOne<any>({
collection: collectionConfig.slug,
req,
where:
loginWithUsername && username
? { username: { equals: username } }
: { email: { equals: unsanitizedEmail.toLowerCase() } },
where: whereConstraint,
})
if (!user || (args.collection.config.auth.verify && user._verified === false)) {
throw new AuthenticationError(req.t, loginWithUsername)
throw new AuthenticationError(req.t, Boolean(canLoginWithUsername && sanitizedUsername))
}
if (user && isLocked(user.lockUntil)) {
if (user && isLocked(new Date(user.lockUntil).getTime())) {
throw new LockedAuth(req.t)
}
@@ -160,7 +218,7 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
const fieldsToSign = getFieldsToSign({
collectionConfig,
email,
email: sanitizedEmail,
user,
})

View File

@@ -120,5 +120,3 @@ export const resetPasswordOperation = async (args: Arguments): Promise<Result> =
throw error
}
}
export default resetPasswordOperation

View File

@@ -1,7 +1,11 @@
import httpStatus from 'http-status'
import type { Collection } from '../../collections/config/types.js'
import type { PayloadRequest } from '../../types/index.js'
import type {
AuthOperationsFromCollectionSlug,
Collection,
} from '../../collections/config/types.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest, Where } from '../../types/index.js'
import { APIError } from '../../errors/index.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
@@ -10,20 +14,16 @@ import { killTransaction } from '../../utilities/killTransaction.js'
import executeAccess from '../executeAccess.js'
import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts.js'
export type Args = {
export type Arguments<TSlug extends CollectionSlug> = {
collection: Collection
data: {
email: string
}
data: AuthOperationsFromCollectionSlug<TSlug>['unlock']
overrideAccess?: boolean
req: PayloadRequest
}
export const unlockOperation = async (args: Args): Promise<boolean> => {
if (!Object.prototype.hasOwnProperty.call(args.data, 'email')) {
throw new APIError('Missing email.', httpStatus.BAD_REQUEST)
}
export const unlockOperation = async <TSlug extends CollectionSlug>(
args: Arguments<TSlug>,
): Promise<boolean> => {
const {
collection: { config: collectionConfig },
overrideAccess,
@@ -31,6 +31,25 @@ export const unlockOperation = async (args: Args): Promise<boolean> => {
req,
} = args
const loginWithUsername = collectionConfig.auth.loginWithUsername
const canLoginWithUsername = Boolean(loginWithUsername)
const canLoginWithEmail = !loginWithUsername || loginWithUsername.allowEmailLogin
const sanitizedEmail = canLoginWithEmail && (args.data?.email || '').toLowerCase().trim()
const sanitizedUsername =
(canLoginWithUsername &&
'username' in args.data &&
typeof args.data.username === 'string' &&
args.data.username.toLowerCase().trim()) ||
null
if (!sanitizedEmail && !sanitizedUsername) {
throw new APIError(
`Missing ${collectionConfig.auth.loginWithUsername ? 'username' : 'email'}.`,
httpStatus.BAD_REQUEST,
)
}
try {
const shouldCommit = await initTransaction(req)
@@ -42,23 +61,31 @@ export const unlockOperation = async (args: Args): Promise<boolean> => {
await executeAccess({ req }, collectionConfig.access.unlock)
}
const options = { ...args }
const { data } = options
// /////////////////////////////////////
// Unlock
// /////////////////////////////////////
if (!data.email) {
throw new APIError('Missing email.', httpStatus.BAD_REQUEST)
let whereConstraint: Where = {}
if (canLoginWithEmail && sanitizedEmail) {
whereConstraint = {
email: {
equals: sanitizedEmail,
},
}
} else if (canLoginWithUsername && sanitizedUsername) {
whereConstraint = {
username: {
equals: sanitizedUsername,
},
}
}
const user = await req.payload.db.findOne({
collection: collectionConfig.slug,
locale,
req,
where: { email: { equals: data.email.toLowerCase() } },
where: whereConstraint,
})
let result
@@ -83,5 +110,3 @@ export const unlockOperation = async (args: Args): Promise<boolean> => {
throw error
}
}
export default unlockOperation

View File

@@ -118,6 +118,11 @@ export type AuthStrategy = {
name: string
}
export type LoginWithUsernameOptions = {
allowEmailLogin?: boolean
requireEmail?: boolean
}
export interface IncomingAuthType {
cookies?: {
domain?: string
@@ -131,7 +136,7 @@ export interface IncomingAuthType {
generateEmailSubject?: GenerateForgotPasswordEmailSubject
}
lockTime?: number
loginWithUsername?: boolean
loginWithUsername?: LoginWithUsernameOptions | boolean
maxLoginAttempts?: number
removeTokenFromResponses?: true
strategies?: AuthStrategy[]
@@ -150,11 +155,13 @@ export type VerifyConfig = {
generateEmailSubject?: GenerateVerifyEmailSubject
}
export interface Auth extends Omit<DeepRequired<IncomingAuthType>, 'forgotPassword' | 'verify'> {
export interface Auth
extends Omit<DeepRequired<IncomingAuthType>, 'forgotPassword' | 'loginWithUsername' | 'verify'> {
forgotPassword?: {
generateEmailHTML?: GenerateForgotPasswordEmailHTML
generateEmailSubject?: GenerateForgotPasswordEmailSubject
}
loginWithUsername: LoginWithUsernameOptions | false
verify?: VerifyConfig | boolean
}

View File

@@ -1,3 +1,5 @@
import type { LoginWithUsernameOptions } from '../../auth/types.js'
import defaultAccess from '../../auth/defaultAccess.js'
export const defaults = {
@@ -59,3 +61,8 @@ export const authDefaults = {
tokenExpiration: 7200,
verify: false,
}
export const loginWithUsernameDefaults: LoginWithUsernameOptions = {
allowEmailLogin: false,
requireEmail: false,
}

View File

@@ -3,11 +3,7 @@ import merge from 'deepmerge'
import type { Config, SanitizedConfig } from '../../config/types.js'
import type { CollectionConfig, SanitizedCollectionConfig } from './types.js'
import baseAccountLockFields from '../../auth/baseFields/accountLock.js'
import baseAPIKeyFields from '../../auth/baseFields/apiKey.js'
import baseAuthFields from '../../auth/baseFields/auth.js'
import baseLoginField from '../../auth/baseFields/loginField.js'
import baseVerificationFields from '../../auth/baseFields/verification.js'
import { getBaseAuthFields } from '../../auth/getAuthFields.js'
import { TimestampsRequired } from '../../errors/TimestampsRequired.js'
import { sanitizeFields } from '../../fields/config/sanitize.js'
import { fieldAffectsData } from '../../fields/config/types.js'
@@ -17,7 +13,7 @@ import { formatLabels } from '../../utilities/formatLabels.js'
import { isPlainObject } from '../../utilities/isPlainObject.js'
import baseVersionFields from '../../versions/baseFields.js'
import { versionDefaults } from '../../versions/defaults.js'
import { authDefaults, defaults } from './defaults.js'
import { authDefaults, defaults, loginWithUsernameDefaults } from './defaults.js'
export const sanitizeCollection = async (
config: Config,
@@ -141,27 +137,8 @@ export const sanitizeCollection = async (
isMergeableObject: isPlainObject,
})
let authFields = []
if (sanitized.auth.useAPIKey) {
authFields = authFields.concat(baseAPIKeyFields)
}
if (!sanitized.auth.disableLocalStrategy) {
const loginField = sanitized.auth.loginWithUsername ? 'username' : 'email'
authFields = authFields.concat(baseLoginField(loginField))
authFields = authFields.concat(baseAuthFields)
if (sanitized.auth.verify) {
if (sanitized.auth.verify === true) sanitized.auth.verify = {}
authFields = authFields.concat(baseVerificationFields)
}
if (sanitized.auth.maxLoginAttempts > 0) {
authFields = authFields.concat(baseAccountLockFields)
}
if (!sanitized.auth.disableLocalStrategy && sanitized.auth.verify === true) {
sanitized.auth.verify = {}
}
// disable duplicate for auth enabled collections by default
@@ -171,7 +148,16 @@ export const sanitizeCollection = async (
sanitized.auth.strategies = []
}
sanitized.fields = mergeBaseFields(sanitized.fields, authFields)
sanitized.auth.loginWithUsername = sanitized.auth.loginWithUsername
? merge(
loginWithUsernameDefaults,
typeof sanitized.auth.loginWithUsername === 'boolean'
? {}
: sanitized.auth.loginWithUsername,
)
: false
sanitized.fields = mergeBaseFields(sanitized.fields, getBaseAuthFields(sanitized.auth))
}
return sanitized as SanitizedCollectionConfig

View File

@@ -92,7 +92,13 @@ const collectionSchema = joi.object().keys({
generateEmailSubject: joi.func(),
}),
lockTime: joi.number(),
loginWithUsername: joi.boolean(),
loginWithUsername: joi.alternatives().try(
joi.boolean(),
joi.object().keys({
allowEmailLogin: joi.boolean(),
requireEmail: joi.boolean(),
}),
),
maxLoginAttempts: joi.number(),
removeTokenFromResponses: joi.boolean().valid(true),
strategies: joi.array().items(

View File

@@ -214,10 +214,6 @@ export const createOperation = async <TSlug extends CollectionSlug>(
let doc
if (collectionConfig.auth && !collectionConfig.auth.disableLocalStrategy) {
if (data.email) {
resultWithLocales.email = (data.email as string).toLowerCase()
}
if (collectionConfig.auth.verify) {
resultWithLocales._verified = Boolean(resultWithLocales._verified) || false
resultWithLocales._verificationToken = crypto.randomBytes(20).toString('hex')
@@ -260,7 +256,7 @@ export const createOperation = async <TSlug extends CollectionSlug>(
// Send verification email if applicable
// /////////////////////////////////////
if (collectionConfig.auth && collectionConfig.auth.verify) {
if (collectionConfig.auth && collectionConfig.auth.verify && result.email) {
await sendVerificationEmail({
collection: { config: collectionConfig },
config: payload.config,

View File

@@ -126,6 +126,31 @@ export const email: Validate<string, unknown, unknown, EmailField> = (
return true
}
export const username: Validate<string, unknown, unknown, TextField> = (
value,
{
req: {
payload: { config },
t,
},
required,
},
) => {
let maxLength: number
if (typeof config?.defaultMaxTextLength === 'number') maxLength = config.defaultMaxTextLength
if (value && maxLength && value.length > maxLength) {
return t('validation:shorterThanMax', { maxLength })
}
if ((value && !/^[\w.-]+$/.test(value)) || (!value && required)) {
return t('validation:username')
}
return true
}
export const textarea: Validate<string, unknown, unknown, TextareaField> = (
value,
{

View File

@@ -74,12 +74,14 @@ export interface GeneratedTypes {
login: {
email: string
password: string
username?: string
}
registerFirstUser: {
email: string
password: string
}
unlock: {
email: string
}
}
}
collectionsUntyped: {
@@ -349,7 +351,7 @@ export class BasePayload {
options: UnlockOptions<TSlug>,
): Promise<boolean> => {
const { unlock } = localOperations.auth
return unlock(this, options)
return unlock<TSlug>(this, options)
}
updateGlobal = async <TSlug extends GlobalSlug>(
@@ -708,11 +710,11 @@ export type {
AfterErrorHook as CollectionAfterErrorHook,
AfterForgotPasswordHook as CollectionAfterForgotPasswordHook,
AfterLoginHook as CollectionAfterLoginHook,
AfterLogoutHook,
AfterMeHook,
AfterLogoutHook as CollectionAfterLogoutHook,
AfterMeHook as CollectionAfterMeHook,
AfterOperationHook as CollectionAfterOperationHook,
AfterReadHook as CollectionAfterReadHook,
AfterRefreshHook,
AfterRefreshHook as CollectionAfterRefreshHook,
AuthCollection,
AuthOperationsFromCollectionSlug,
BeforeChangeHook as CollectionBeforeChangeHook,

View File

@@ -1,12 +1,31 @@
import type { SharpOptions } from 'sharp'
import type { SanitizedConfig } from '../config/types.js'
import type { PayloadRequest } from '../types/index.js'
import type { UploadEdits } from './types.js'
export const percentToPixel = (value, dimension) => {
return Math.floor((parseFloat(value) / 100) * dimension)
}
export async function cropImage({ cropData, dimensions, file, sharp }) {
type CropImageArgs = {
cropData: UploadEdits['crop']
dimensions: { height: number; width: number }
file: PayloadRequest['file']
heightInPixels: number
sharp: SanitizedConfig['sharp']
widthInPixels: number
}
export async function cropImage({
cropData,
dimensions,
file,
heightInPixels,
sharp,
widthInPixels,
}: CropImageArgs) {
try {
const { heightPixels, widthPixels, x, y } = cropData
const { x, y } = cropData
const fileIsAnimatedType = ['image/avif', 'image/gif', 'image/webp'].includes(file.mimetype)
@@ -15,10 +34,10 @@ export async function cropImage({ cropData, dimensions, file, sharp }) {
if (fileIsAnimatedType) sharpOptions.animated = true
const formattedCropData = {
height: Number(heightPixels),
height: Number(heightInPixels),
left: percentToPixel(x, dimensions.width),
top: percentToPixel(y, dimensions.height),
width: Number(widthPixels),
width: Number(widthInPixels),
}
const cropped = sharp(file.tempFilePath || file.data, sharpOptions).extract(formattedCropData)

View File

@@ -203,7 +203,14 @@ export const generateFileData = async <T>({
let fileForResize = file
if (cropData && sharp) {
const { data: croppedImage, info } = await cropImage({ cropData, dimensions, file, sharp })
const { data: croppedImage, info } = await cropImage({
cropData,
dimensions,
file,
heightInPixels: uploadEdits.heightInPixels,
sharp,
widthInPixels: uploadEdits.widthInPixels,
})
filesToSave.push({
buffer: croppedImage,

View File

@@ -1,4 +1,4 @@
import type { OutputInfo, Sharp, SharpOptions } from 'sharp'
import type { Sharp, Metadata as SharpMetadata, SharpOptions } from 'sharp'
import { fileTypeFromBuffer } from 'file-type'
import fs from 'fs'
@@ -68,11 +68,20 @@ const getSanitizedImageData = (sourceImage: string): SanitizedImageData => {
* @param extension - the extension to use
* @returns the new image name that is not taken
*/
const createImageName = (
outputImageName: string,
{ height, width }: OutputInfo,
extension: string,
) => `${outputImageName}-${width}x${height}.${extension}`
type CreateImageNameArgs = {
extension: string
height: number
outputImageName: string
width: number
}
const createImageName = ({
extension,
height,
outputImageName,
width,
}: CreateImageNameArgs): string => {
return `${outputImageName}-${width}x${height}.${extension}`
}
type CreateResultArgs = {
filename?: FileSize['filename']
@@ -122,71 +131,61 @@ const createResult = ({
}
/**
* Check if the image needs to be resized according to the requested dimensions
* and the original image size. If the resize options withoutEnlargement or withoutReduction are provided,
* the image will be resized regardless of the requested dimensions, given that the
* width or height to be resized is provided.
* Determine whether or not to resize the image.
* - resize using image config
* - resize using image config with focal adjustments
* - do not resize at all
*
* @param resizeConfig - object containing the requested dimensions and resize options
* @param original - the original image size
* @returns true if resizing is not needed, false otherwise
*/
const preventResize = (
{ height: desiredHeight, width: desiredWidth, withoutEnlargement, withoutReduction }: ImageSize,
original: ProbedImageSize,
): boolean => {
// default is to allow reduction
if (withoutReduction !== undefined) {
return false // needs resize
}
// default is to prevent enlargement
if (withoutEnlargement !== undefined) {
return false // needs resize
}
const isWidthOrHeightNotDefined = !desiredHeight || !desiredWidth
if (isWidthOrHeightNotDefined) {
// If width and height are not defined, it means there is a format conversion
// and the image needs to be "resized" (transformed).
return false // needs resize
}
const hasInsufficientWidth = desiredWidth > original.width
const hasInsufficientHeight = desiredHeight > original.height
if (hasInsufficientWidth && hasInsufficientHeight) {
// doesn't need resize - prevent enlargement. This should only happen if both width and height are insufficient.
// if only one dimension is insufficient and the other is sufficient, resizing needs to happen, as the image
// should be resized to the sufficient dimension.
return true // do not create a new size
}
return false // needs resize
}
/**
* Check if the image should be passed directly to sharp without payload adjusting properties.
* `imageResizeConfig.withoutEnlargement`:
* - undefined [default]: uploading images with smaller width AND height than the image size will return null
* - false: always enlarge images to the image size
* - true: if the image is smaller than the image size, return the original image
*
* @param resizeConfig - object containing the requested dimensions and resize options
* @param original - the original image size
* @returns true if the image should passed directly to sharp
* `imageResizeConfig.withoutReduction`:
* - false [default]: always enlarge images to the image size
* - true: if the image is smaller than the image size, return the original image
*
* @return 'omit' | 'resize' | 'resizeWithFocalPoint'
*/
const applyPayloadAdjustments = (
{ fit, height, width, withoutEnlargement, withoutReduction }: ImageSize,
original: ProbedImageSize,
) => {
if (fit === 'contain' || fit === 'inside') return false
if (!isNumber(height) && !isNumber(width)) return false
const getImageResizeAction = ({
dimensions: originalImage,
hasFocalPoint,
imageResizeConfig,
}: {
dimensions: ProbedImageSize
hasFocalPoint?: boolean
imageResizeConfig: ImageSize
}): 'omit' | 'resize' | 'resizeWithFocalPoint' => {
const {
fit,
height: targetHeight,
width: targetWidth,
withoutEnlargement,
withoutReduction,
} = imageResizeConfig
const targetAspectRatio = width / height
const originalAspectRatio = original.width / original.height
if (originalAspectRatio === targetAspectRatio) return false
// prevent upscaling by default when x and y are both smaller than target image size
if (targetHeight && targetWidth) {
const originalImageIsSmallerXAndY =
originalImage.width < targetWidth && originalImage.height < targetHeight
if (withoutEnlargement === undefined && originalImageIsSmallerXAndY) {
return 'omit' // prevent image size from being enlarged
}
}
const skipEnlargement = withoutEnlargement && (original.height < height || original.width < width)
const skipReduction = withoutReduction && (original.height > height || original.width > width)
if (skipEnlargement || skipReduction) return false
const originalImageIsSmallerXOrY =
originalImage.width < targetWidth || originalImage.height < targetHeight
if (fit === 'contain' || fit === 'inside') return 'resize'
if (!isNumber(targetHeight) && !isNumber(targetWidth)) return 'resize'
return true
const targetAspectRatio = targetWidth / targetHeight
const originalAspectRatio = originalImage.width / originalImage.height
if (originalAspectRatio === targetAspectRatio) return 'resize'
if (withoutEnlargement && originalImageIsSmallerXOrY) return 'resize'
if (withoutReduction && !originalImageIsSmallerXOrY) return 'resize'
return hasFocalPoint ? 'resizeWithFocalPoint' : 'resize'
}
/**
@@ -209,6 +208,19 @@ const sanitizeResizeConfig = (resizeConfig: ImageSize): ImageSize => {
return resizeConfig
}
/**
* Used to extract height from images, animated or not.
*
* @param sharpMetadata - the sharp metadata
* @returns the height of the image
*/
function extractHeightFromImage(sharpMetadata: SharpMetadata): number {
if (sharpMetadata?.pages) {
return sharpMetadata.height / sharpMetadata.pages
}
return sharpMetadata.height
}
/**
* For the provided image sizes, handle the resizing and the transforms
* (format, trim, etc.) of each requested image size and return the result object.
@@ -261,24 +273,28 @@ export async function resizeAndTransformImageSizes({
if (fileIsAnimatedType) sharpOptions.animated = true
const sharpBase: Sharp | undefined = sharp(file.tempFilePath || file.data, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
const originalImageMeta = await sharpBase.metadata()
const resizeImageMeta = {
height: extractHeightFromImage(originalImageMeta),
width: originalImageMeta.width,
}
const results: ImageSizesResult[] = await Promise.all(
imageSizes.map(async (imageResizeConfig): Promise<ImageSizesResult> => {
imageResizeConfig = sanitizeResizeConfig(imageResizeConfig)
// This checks if a resize should happen. If not, the resized image will be
// skipped COMPLETELY and thus will not be included in the resulting images.
// All further format/trim options will thus be skipped as well.
if (preventResize(imageResizeConfig, dimensions)) {
return createResult({ name: imageResizeConfig.name })
}
const resizeAction = getImageResizeAction({
dimensions,
hasFocalPoint: Boolean(incomingFocalPoint),
imageResizeConfig,
})
if (resizeAction === 'omit') return createResult({ name: imageResizeConfig.name })
const imageToResize = sharpBase.clone()
let resized = imageToResize
const metadata = await sharpBase.metadata()
if (incomingFocalPoint && applyPayloadAdjustments(imageResizeConfig, dimensions)) {
if (resizeAction === 'resizeWithFocalPoint') {
let { height: resizeHeight, width: resizeWidth } = imageResizeConfig
const originalAspectRatio = dimensions.width / dimensions.height
@@ -293,44 +309,62 @@ export async function resizeAndTransformImageSizes({
resizeHeight = Math.round(resizeWidth / originalAspectRatio)
}
// Scale the image up or down to fit the resize dimensions
const scaledImage = imageToResize.resize({
height: resizeHeight,
width: resizeWidth,
})
if (!resizeHeight) resizeHeight = resizeImageMeta.height
if (!resizeWidth) resizeWidth = resizeImageMeta.width
const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true })
// if requested image is larger than the incoming size, then resize using sharp and then extract with focal point
if (resizeHeight > resizeImageMeta.height || resizeWidth > resizeImageMeta.width) {
const resizeAspectRatio = resizeWidth / resizeHeight
const prioritizeHeight = resizeAspectRatio < originalAspectRatio
resized = imageToResize.resize({
height: prioritizeHeight ? resizeHeight : undefined,
width: prioritizeHeight ? undefined : resizeWidth,
})
const safeResizeWidth = resizeWidth ?? scaledImageInfo.width
const maxOffsetX = scaledImageInfo.width - safeResizeWidth
const leftFocalEdge = Math.round(
scaledImageInfo.width * (incomingFocalPoint.x / 100) - safeResizeWidth / 2,
)
const safeOffsetX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX)
const isAnimated = fileIsAnimatedType && metadata.pages
let safeResizeHeight = resizeHeight ?? scaledImageInfo.height
if (isAnimated && resizeHeight === undefined) {
safeResizeHeight = scaledImageInfo.height / metadata.pages
// must read from buffer, resize.metadata will return the original image metadata
const { info } = await resized.toBuffer({ resolveWithObject: true })
resizeImageMeta.height = extractHeightFromImage({
...originalImageMeta,
height: info.height,
})
resizeImageMeta.width = info.width
}
const maxOffsetY = isAnimated
? safeResizeHeight - (resizeHeight ?? safeResizeHeight)
: scaledImageInfo.height - safeResizeHeight
const halfResizeX = resizeWidth / 2
const xFocalCenter = resizeImageMeta.width * (incomingFocalPoint.x / 100)
const calculatedRightPixelBound = xFocalCenter + halfResizeX
let leftBound = xFocalCenter - halfResizeX
const topFocalEdge = Math.round(
scaledImageInfo.height * (incomingFocalPoint.y / 100) - safeResizeHeight / 2,
)
const safeOffsetY = Math.min(Math.max(0, topFocalEdge), maxOffsetY)
// if the right bound is greater than the image width, adjust the left bound
// keeping focus on the right
if (calculatedRightPixelBound > resizeImageMeta.width) {
leftBound = resizeImageMeta.width - resizeWidth
}
// extract the focal area from the scaled image
resized = (fileIsAnimatedType ? imageToResize : scaledImage).extract({
height: safeResizeHeight,
left: safeOffsetX,
top: safeOffsetY,
width: safeResizeWidth,
// if the left bound is less than 0, adjust the left bound to 0
// keeping the focus on the left
if (leftBound < 0) leftBound = 0
const halfResizeY = resizeHeight / 2
const yFocalCenter = resizeImageMeta.height * (incomingFocalPoint.y / 100)
const calculatedBottomPixelBound = yFocalCenter + halfResizeY
let topBound = yFocalCenter - halfResizeY
// if the bottom bound is greater than the image height, adjust the top bound
// keeping the image as far right as possible
if (calculatedBottomPixelBound > resizeImageMeta.height) {
topBound = resizeImageMeta.height - resizeHeight
}
// if the top bound is less than 0, adjust the top bound to 0
// keeping the image focus near the top
if (topBound < 0) topBound = 0
resized = resized.extract({
height: resizeHeight,
left: Math.floor(leftBound),
top: Math.floor(topBound),
width: resizeWidth,
})
} else {
resized = imageToResize.resize(imageResizeConfig)
@@ -359,11 +393,15 @@ export async function resizeAndTransformImageSizes({
const mimeInfo = await fileTypeFromBuffer(bufferData)
const imageNameWithDimensions = createImageName(
sanitizedImage.name,
bufferInfo,
mimeInfo?.ext || sanitizedImage.ext,
)
const imageNameWithDimensions = createImageName({
extension: mimeInfo?.ext || sanitizedImage.ext,
height: extractHeightFromImage({
...originalImageMeta,
height: bufferInfo.height,
}),
outputImageName: sanitizedImage.name,
width: bufferInfo.width,
})
const imagePath = `${staticPath}/${imageNameWithDimensions}`
@@ -380,7 +418,8 @@ export async function resizeAndTransformImageSizes({
name: imageResizeConfig.name,
filename: imageNameWithDimensions,
filesize: size,
height: fileIsAnimatedType && metadata.pages ? height / metadata.pages : height,
height:
fileIsAnimatedType && originalImageMeta.pages ? height / originalImageMeta.pages : height,
mimeType: mimeInfo?.mime || mimeType,
sizesToSave: [{ buffer: bufferData, path: imagePath }],
width,

View File

@@ -189,15 +189,22 @@ export type FileToSave = {
path: string
}
export type UploadEdits = {
crop?: {
height?: number
width?: number
x?: number
y?: number
}
focalPoint?: {
x?: number
y?: number
}
type Crop = {
height: number
unit: '%' | 'px'
width: number
x: number
y: number
}
type FocalPoint = {
x: number
y: number
}
export type UploadEdits = {
crop?: Crop
focalPoint?: FocalPoint
heightInPixels?: number
widthInPixels?: number
}

View File

@@ -3,6 +3,7 @@ import type { JSONSchema4, JSONSchema4TypeName } from 'json-schema'
import pluralize from 'pluralize'
const { singular } = pluralize
import type { Auth } from '../auth/types.js'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'
import type { Field, FieldAffectingData, Option } from '../fields/config/types.js'
@@ -607,60 +608,69 @@ export function entityToJSONSchema(
}
}
function generateOperationJSONSchema(
config: SanitizedCollectionConfig,
operation: 'forgotPassword' | 'login' | 'registerFirstUser',
): JSONSchema4 {
const usernameLogin = config.auth?.loginWithUsername
const fieldType: JSONSchema4 = {
type: 'string',
const fieldType: JSONSchema4 = {
type: 'string',
required: false,
}
const generateAuthFieldTypes = (
loginWithUsername: Auth['loginWithUsername'],
withPassword = false,
): JSONSchema4 => {
const passwordField = {
password: fieldType,
}
let properties: JSONSchema4['properties'] = {}
switch (operation) {
case 'login': {
properties = {
password: fieldType,
[usernameLogin ? 'username' : 'email']: fieldType,
if (loginWithUsername) {
if (loginWithUsername.allowEmailLogin) {
return {
additionalProperties: false,
oneOf: [
{
additionalProperties: false,
properties: { email: fieldType, ...(withPassword ? { password: fieldType } : {}) },
required: ['email', ...(withPassword ? ['password'] : [])],
},
{
additionalProperties: false,
properties: { username: fieldType, ...(withPassword ? { password: fieldType } : {}) },
required: ['username', ...(withPassword ? ['password'] : [])],
},
],
}
break
}
case 'forgotPassword': {
properties = {
[usernameLogin ? 'username' : 'email']: fieldType,
}
break
}
case 'registerFirstUser': {
properties = {
email: fieldType,
password: fieldType,
}
if (usernameLogin) properties.username = fieldType
break
return {
additionalProperties: false,
properties: {
username: fieldType,
...(withPassword ? { password: fieldType } : {}),
},
required: ['username', ...(withPassword ? ['password'] : [])],
}
}
return {
additionalProperties: false,
properties,
required: Object.keys(properties),
properties: {
email: fieldType,
...(withPassword ? { password: fieldType } : {}),
},
required: ['email', ...(withPassword ? ['password'] : [])],
}
}
export function authCollectionToOperationsJSONSchema(
config: SanitizedCollectionConfig,
): JSONSchema4 {
const properties = {
forgotPassword: {
...generateOperationJSONSchema(config, 'forgotPassword'),
},
login: {
...generateOperationJSONSchema(config, 'login'),
},
registerFirstUser: {
...generateOperationJSONSchema(config, 'registerFirstUser'),
},
const loginWithUsername = config.auth?.loginWithUsername
const generatedFields: JSONSchema4 = generateAuthFieldTypes(loginWithUsername)
const generatedFieldsWithPassword: JSONSchema4 = generateAuthFieldTypes(loginWithUsername, true)
const properties: JSONSchema4['properties'] = {
forgotPassword: generatedFields,
login: generatedFieldsWithPassword,
registerFirstUser: generatedFieldsWithPassword,
unlock: generatedFields,
}
return {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "The official Nested Docs plugin for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-relationship-object-ids",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "A Payload plugin to store all relationship IDs as ObjectIDs",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Search plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "SEO plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",

View File

@@ -30,9 +30,13 @@ async function build() {
//external: ['*.svg'],
plugins: [sassPlugin({ css: 'external' })],
})
await fs.rename('dist/field/index.css', 'dist/exports/client/bundled.css', (err) => {
if (err) console.error(`Error while renaming index.css: ${err}`)
})
try {
fs.renameSync('dist/field/index.css', 'dist/exports/client/bundled.css')
} catch (err) {
console.error(`Error while renaming index.css: ${err}`)
throw err
}
console.log('dist/field/bundled.css bundled successfully')

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "The officially supported Slate richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-azure",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Payload storage adapter for Azure Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-gcs",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Payload storage adapter for Google Cloud Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-s3",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Payload storage adapter for Amazon S3",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-uploadthing",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Payload storage adapter for uploadthing",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-vercel-blob",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"description": "Payload storage adapter for Vercel Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/translations",
"version": "3.0.0-beta.65",
"version": "3.0.0-beta.67",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -30,8 +30,12 @@ export async function translateText(text: string, targetLang: string) {
})
try {
const data = await response.json()
console.log(' Old text:', text, 'New text:', data.choices[0].message.content.trim())
return data.choices[0].message.content.trim()
if (data?.choices?.[0]) {
console.log(' Old text:', text, 'New text:', data.choices[0].message.content.trim())
return data.choices[0].message.content.trim()
} else {
console.log(`Could not translate: ${text} in lang: ${targetLang}`)
}
} catch (e) {
console.error('Error translating:', text, 'to', targetLang, 'response', response, '. Error:', e)
throw e

View File

@@ -21,6 +21,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'authentication:createFirstUser',
'authentication:emailNotValid',
'authentication:usernameNotValid',
'authentication:emailOrUsername',
'authentication:emailSent',
'authentication:emailVerified',
'authentication:enableAPIKey',
@@ -230,6 +231,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:true',
'general:users',
'general:user',
'general:username',
'general:unauthorized',
'general:unsavedChangesDuplicate',
'general:untitled',
@@ -281,6 +283,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'validation:required',
'validation:requiresAtLeast',
'validation:shorterThanMax',
'validation:username',
'version:aboutToPublishSelection',
'version:aboutToRestore',

View File

@@ -18,6 +18,7 @@ export const arTranslations: DefaultTranslationsObject = {
confirmPassword: 'تأكيد كلمة المرور',
createFirstUser: 'إنشاء المستخدم الأوّل',
emailNotValid: 'البريد الإلكتروني غير صالح',
emailOrUsername: 'البريد الإلكتروني أو اسم المستخدم',
emailSent: 'تمّ ارسال البريد الإلكتروني',
emailVerified: 'تم التحقق من البريد الإلكتروني بنجاح.',
enableAPIKey: 'تفعيل مفتاح API',
@@ -294,6 +295,7 @@ export const arTranslations: DefaultTranslationsObject = {
updating: 'جار التحديث',
uploading: 'جار الرفع',
user: 'المستخدم',
username: 'اسم المستخدم',
users: 'المستخدمين',
value: 'القيمة',
welcome: 'مرحبًا',
@@ -356,6 +358,8 @@ export const arTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'هذا الحقل يتطلب رقمين.',
shorterThanMax: 'يجب أن تكون هذه القيمة أقصر من الحد الأقصى للطول الذي هو {{maxLength}} أحرف.',
trueOrFalse: 'يمكن أن يكون هذا الحقل مساويًا فقط للقيمتين صحيح أو خطأ.',
username:
'يرجى إدخال اسم مستخدم صالح. يمكن أن يحتوي على أحرف، أرقام، شرطات، فواصل وشرطات سفلية.',
validUploadID: 'هذا الحقل ليس معرّف تحميل صالح.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const azTranslations: DefaultTranslationsObject = {
confirmPassword: 'Şifrəni təsdiq et',
createFirstUser: 'İlk istifadəçini yaradın',
emailNotValid: 'Təqdim olunan e-poçt etibarlı deyil',
emailOrUsername: 'E-poçt və ya İstifadəçi adı',
emailSent: 'E-poçt göndərildi',
emailVerified: 'Email uğurla təsdiqləndi.',
enableAPIKey: 'API açarını aktivləşdir',
@@ -297,6 +298,7 @@ export const azTranslations: DefaultTranslationsObject = {
updating: 'Yenilənir',
uploading: 'Yüklənir',
user: 'İstifadəçi',
username: 'İstifadəçi adı',
users: 'İstifadəçilər',
value: 'Dəyər',
welcome: 'Xoş gəldiniz',
@@ -361,6 +363,8 @@ export const azTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Bu sahə iki nömrə tələb edir.',
shorterThanMax: 'Bu dəyər {{maxLength}} simvoldan qısa olmalıdır.',
trueOrFalse: 'Bu sahə yalnız doğru və ya yanlış ola bilər.',
username:
'Zəhmət olmasa, etibarlı bir istifadəçi adı daxil edin. Hərflər, rəqəmlər, tire, nöqtə və alt xəttlər ola bilər.',
validUploadID: 'Bu sahə doğru yükləmə ID-si deyil.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const bgTranslations: DefaultTranslationsObject = {
confirmPassword: 'Потвърди парола',
createFirstUser: 'Създай първи потребител',
emailNotValid: 'Даденият имейл не е валиден',
emailOrUsername: 'Имейл или Потребителско име',
emailSent: 'Имейлът е изпратен',
emailVerified: 'Успешно потвърден имейл.',
enableAPIKey: 'Активирай API ключ',
@@ -295,6 +296,7 @@ export const bgTranslations: DefaultTranslationsObject = {
updating: 'Обновява се',
uploading: 'Качва се',
user: 'Потребител',
username: 'Потребителско име',
users: 'Потребители',
value: 'Стойност',
welcome: 'Добре дошъл',
@@ -361,6 +363,8 @@ export const bgTranslations: DefaultTranslationsObject = {
shorterThanMax:
'Тази стойност трябва да е по-малка от максималната стойност от {{maxLength}} символа.',
trueOrFalse: 'Това поле може да бъде само "true" или "false".',
username:
'Моля, въведете валидно потребителско име. Може да съдържа букви, цифри, тирета, точки и долни черти.',
validUploadID: 'Това поле не е валиден идентификатор на качването.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const csTranslations: DefaultTranslationsObject = {
confirmPassword: 'Potvrdit heslo',
createFirstUser: 'Vytvořit prvního uživatele',
emailNotValid: 'Zadaný email není platný',
emailOrUsername: 'E-mail nebo Uživatelské jméno',
emailSent: 'Email odeslán',
emailVerified: 'E-mail úspěšně ověřen.',
enableAPIKey: 'Povolit API klíč',
@@ -295,6 +296,7 @@ export const csTranslations: DefaultTranslationsObject = {
updating: 'Aktualizace',
uploading: 'Nahrávání',
user: 'Uživatel',
username: 'Uživatelské jméno',
users: 'Uživatelé',
value: 'Hodnota',
welcome: 'Vítejte',
@@ -359,6 +361,8 @@ export const csTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Toto pole vyžaduje dvě čísla.',
shorterThanMax: 'Tato hodnota musí být kratší než maximální délka {{maxLength}} znaků.',
trueOrFalse: 'Toto pole může být rovno pouze true nebo false.',
username:
'Prosím, zadejte platné uživatelské jméno. Může obsahovat písmena, čísla, pomlčky, tečky a podtržítka.',
validUploadID: 'Toto pole není platné ID pro odeslání.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const deTranslations: DefaultTranslationsObject = {
confirmPassword: 'Passwort bestätigen',
createFirstUser: 'Ersten Benutzer erstellen',
emailNotValid: 'Die angegebene E-Mail-Adresse ist ungültig',
emailOrUsername: 'E-Mail oder Benutzername',
emailSent: 'E-Mail verschickt',
emailVerified: 'E-Mail erfolgreich verifiziert.',
enableAPIKey: 'API-Key aktivieren',
@@ -301,6 +302,7 @@ export const deTranslations: DefaultTranslationsObject = {
updating: 'Aktualisierung',
uploading: 'Hochladen',
user: 'Benutzer',
username: 'Benutzername',
users: 'Benutzer',
value: 'Wert',
welcome: 'Willkommen',
@@ -365,6 +367,8 @@ export const deTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Dieses Feld muss zwei Nummern enthalten.',
shorterThanMax: 'Dieser Wert muss kürzer als die maximale Länge von {{maxLength}} sein.',
trueOrFalse: 'Dieses Feld kann nur wahr oder falsch sein.',
username:
'Bitte geben Sie einen gültigen Benutzernamen ein. Dieser kann Buchstaben, Zahlen, Bindestriche, Punkte und Unterstriche enthalten.',
validUploadID: "'Dieses Feld enthält keine valide Upload-ID.'",
},
version: {

View File

@@ -18,6 +18,7 @@ export const enTranslations = {
confirmPassword: 'Confirm Password',
createFirstUser: 'Create first user',
emailNotValid: 'The email provided is not valid',
emailOrUsername: 'Email or Username',
emailSent: 'Email Sent',
emailVerified: 'Email verified successfully.',
enableAPIKey: 'Enable API Key',
@@ -298,6 +299,7 @@ export const enTranslations = {
updating: 'Updating',
uploading: 'Uploading',
user: 'User',
username: 'Username',
users: 'Users',
value: 'Value',
welcome: 'Welcome',
@@ -362,6 +364,8 @@ export const enTranslations = {
requiresTwoNumbers: 'This field requires two numbers.',
shorterThanMax: 'This value must be shorter than the max length of {{maxLength}} characters.',
trueOrFalse: 'This field can only be equal to true or false.',
username:
'Please enter a valid username. Can contain letters, numbers, hyphens, periods and underscores.',
validUploadID: 'This field is not a valid upload ID.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const esTranslations: DefaultTranslationsObject = {
confirmPassword: 'Confirmar Contraseña',
createFirstUser: 'Crear al primer usuario',
emailNotValid: 'El correo proporcionado es inválido',
emailOrUsername: 'Correo electrónico o nombre de usuario',
emailSent: 'Correo Enviado',
emailVerified: 'Correo electrónico verificado con éxito.',
enableAPIKey: 'Habilitar Clave API',
@@ -300,6 +301,7 @@ export const esTranslations: DefaultTranslationsObject = {
updating: 'Actualizando',
uploading: 'Subiendo',
user: 'Usuario',
username: 'Nombre de usuario',
users: 'Usuarios',
value: 'Valor',
welcome: 'Bienvenido',
@@ -364,6 +366,8 @@ export const esTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Este campo requiere dos números.',
shorterThanMax: 'Este dato debe ser más corto que el máximo de {{maxLength}} caracteres.',
trueOrFalse: 'Este campo solamente puede ser verdadero o falso.',
username:
'Por favor, introduzca un nombre de usuario válido. Puede contener letras, números, guiones, puntos y guiones bajos.',
validUploadID: "'Este campo no es una ID de subida válida.'",
},
version: {

View File

@@ -18,6 +18,7 @@ export const faTranslations: DefaultTranslationsObject = {
confirmPassword: 'تأیید گذرواژه',
createFirstUser: 'ایجاد کاربر نخست',
emailNotValid: 'رایانامه ارائه‌شده درست نیست',
emailOrUsername: 'ایمیل یا نام کاربری',
emailSent: 'رایانامه فرستاده شد',
emailVerified: 'ایمیل با موفقیت تایید شد.',
enableAPIKey: 'فعال‌سازی کلید اِی‌پی‌آی',
@@ -295,6 +296,7 @@ export const faTranslations: DefaultTranslationsObject = {
updating: 'در حال به‌روزرسانی',
uploading: 'در حال بارگذاری',
user: 'کاربر',
username: 'نام کاربری',
users: 'کاربران',
value: 'مقدار',
welcome: 'خوش‌آمدید',
@@ -359,6 +361,8 @@ export const faTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'این کادر به دو عدد نیاز دارد.',
shorterThanMax: 'ورودی باید کمتر از {{maxLength}} واژه باشد.',
trueOrFalse: 'این کادر فقط می تواند به صورت true یا false باشد.',
username:
'لطفاً یک نام کاربری معتبر وارد کنید. می تواند شامل حروف، اعداد، خط فاصله، نقاط و خط زیر باشد.',
validUploadID: 'این فیلد یک شناسه بارگذاری معتبر نیست.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const frTranslations: DefaultTranslationsObject = {
confirmPassword: 'Confirmez le mot de passe',
createFirstUser: 'Créer le premier utilisateur',
emailNotValid: 'Ladresse e-mail fournie nest pas valide',
emailOrUsername: "Email ou Nom d'utilisateur",
emailSent: 'E-mail envoyé',
emailVerified: 'E-mail vérifié avec succès.',
enableAPIKey: 'Activer la clé API',
@@ -304,6 +305,7 @@ export const frTranslations: DefaultTranslationsObject = {
updating: 'Mise à jour',
uploading: 'Téléchargement',
user: 'Utilisateur',
username: "Nom d'utilisateur",
users: 'Utilisateurs',
value: 'Valeur',
welcome: 'Bienvenue',
@@ -370,6 +372,8 @@ export const frTranslations: DefaultTranslationsObject = {
shorterThanMax:
'Cette valeur doit être inférieure à la longueur maximale de {{maxLength}} caractères.',
trueOrFalse: 'Ce champ ne peut être égal quà vrai ou faux.',
username:
"Veuillez entrer un nom d'utilisateur valide. Il peut contenir des lettres, des chiffres, des tirets, des points et des tirets bas.",
validUploadID: 'Ce champ nest pas un valide identifiant de fichier.',
},
version: {

View File

@@ -17,6 +17,7 @@ export const heTranslations: DefaultTranslationsObject = {
confirmPassword: 'אישור סיסמה',
createFirstUser: 'יצירת משתמש ראשון',
emailNotValid: 'הדוא"ל שסופק אינו תקין',
emailOrUsername: 'דוא"ל או שם משתמש',
emailSent: 'הודעת דואר נשלחה',
emailVerified: 'דוא"ל אומת בהצלחה.',
enableAPIKey: 'הפעלת מפתח API',
@@ -290,6 +291,7 @@ export const heTranslations: DefaultTranslationsObject = {
updating: 'מעדכן',
uploading: 'מעלה',
user: 'משתמש',
username: 'שם משתמש',
users: 'משתמשים',
value: 'ערך',
welcome: 'ברוך הבא',
@@ -352,6 +354,7 @@ export const heTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'שדה זה דורש שני מספרים.',
shorterThanMax: 'ערך זה חייב להיות קצר מ-{{maxLength}} תווים.',
trueOrFalse: 'שדה זה יכול להיות רק true או false.',
username: 'אנא הזן שם משתמש חוקי. יכול להכיל אותיות, מספרים, מקפים, נקודות וקווים תחתונים.',
validUploadID: 'שדה זה אינו מזהה העלאה תקני.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const hrTranslations: DefaultTranslationsObject = {
confirmPassword: 'Potvrdi lozinku',
createFirstUser: 'Kreiraj prvog korisnika',
emailNotValid: 'Email nije ispravan',
emailOrUsername: 'E-mail ili Korisničko ime',
emailSent: 'Email poslan',
emailVerified: 'Email uspješno provjeren.',
enableAPIKey: 'Omogući API ključ',
@@ -296,6 +297,7 @@ export const hrTranslations: DefaultTranslationsObject = {
updating: 'Ažuriranje',
uploading: 'Prijenos',
user: 'Korisnik',
username: 'Korisničko ime',
users: 'Korisnici',
value: 'Attribute',
welcome: 'Dobrodošli',
@@ -360,6 +362,8 @@ export const hrTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Ovo polje zahtjeva dva broja.',
shorterThanMax: 'Ova vrijednost mora biti kraća od maksimalne dužine od {{maxLength}} znakova',
trueOrFalse: 'Ovo polje može biti samo točno ili netočno',
username:
'Unesite važeće korisničko ime. Može sadržavati slova, brojeve, crtice, točke i donje crte.',
validUploadID: 'Ovo polje nije valjani ID prijenosa.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const huTranslations: DefaultTranslationsObject = {
confirmPassword: 'Jelszó megerősítése',
createFirstUser: 'Első felhasználó létrehozása',
emailNotValid: 'A megadott e-mail cím érvénytelen',
emailOrUsername: 'E-mail vagy Felhasználónév',
emailSent: 'E-mail elküldve',
emailVerified: 'Az email sikeresen megerősítve.',
enableAPIKey: 'API-kulcs engedélyezése',
@@ -298,6 +299,7 @@ export const huTranslations: DefaultTranslationsObject = {
updating: 'Frissítés',
uploading: 'Feltöltés',
user: 'Felhasználó',
username: 'Felhasználónév',
users: 'Felhasználók',
value: 'Érték',
welcome: 'Üdvözöljük',
@@ -364,6 +366,8 @@ export const huTranslations: DefaultTranslationsObject = {
shorterThanMax:
'Ennek az értéknek rövidebbnek kell lennie, mint a maximálisan megengedett {{maxLength}} karakter.',
trueOrFalse: 'Ez a mező csak igaz vagy hamis lehet.',
username:
'Adjon meg egy érvényes felhasználónevet. Tartalmazhat betűket, számokat, kötőjeleket, pontokat és aláhúzásokat.',
validUploadID: 'Ez a mező nem érvényes feltöltési azonosító.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const itTranslations: DefaultTranslationsObject = {
confirmPassword: 'Conferma Password',
createFirstUser: 'Crea il primo utente',
emailNotValid: "L'email fornita non è valida",
emailOrUsername: 'Email o Nome utente',
emailSent: 'Email Inviata',
emailVerified: 'Email verificata con successo.',
enableAPIKey: 'Abilita la Chiave API',
@@ -298,6 +299,7 @@ export const itTranslations: DefaultTranslationsObject = {
updating: 'Aggiornamento',
uploading: 'Caricamento',
user: 'Utente',
username: 'Nome utente',
users: 'Utenti',
value: 'Valore',
welcome: 'Benvenuto',
@@ -364,6 +366,8 @@ export const itTranslations: DefaultTranslationsObject = {
shorterThanMax:
'Questo valore deve essere inferiore alla lunghezza massima di {{maxLength}} caratteri.',
trueOrFalse: "Questo campo può essere solo uguale a 'true' o 'false'.",
username:
'Inserisci un nome utente valido. Può contenere lettere, numeri, trattini, punti e underscore.',
validUploadID: "'Questo campo non è un ID di Upload valido.'",
},
version: {

View File

@@ -18,6 +18,7 @@ export const jaTranslations: DefaultTranslationsObject = {
confirmPassword: 'パスワードの確認',
createFirstUser: '最初のユーザーを作成',
emailNotValid: '入力されたメールアドレスは無効です。',
emailOrUsername: 'メールまたはユーザー名',
emailSent: 'Emailが送信されました。',
emailVerified: 'メールが正常に確認されました。',
enableAPIKey: 'API Keyを許可',
@@ -296,6 +297,7 @@ export const jaTranslations: DefaultTranslationsObject = {
updating: '更新中',
uploading: 'アップロード中',
user: 'ユーザー',
username: 'ユーザーネーム',
users: 'ユーザー',
value: '値',
welcome: 'ようこそ',
@@ -359,6 +361,8 @@ export const jaTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: '2つの数値が必要です。',
shorterThanMax: '{{maxLength}} 文字以下にする必要があります。',
trueOrFalse: '"true" または "false" の値にする必要があります。',
username:
'有効なユーザーネームを入力してください。文字、数字、ハイフン、ピリオド、アンダースコアを使用できます。',
validUploadID: '有効なアップロードIDではありません。',
},
version: {

View File

@@ -18,6 +18,7 @@ export const koTranslations: DefaultTranslationsObject = {
confirmPassword: '비밀번호 확인',
createFirstUser: '첫 번째 사용자 생성',
emailNotValid: '입력한 이메일은 유효하지 않습니다.',
emailOrUsername: '이메일 또는 사용자 이름',
emailSent: '이메일 전송됨',
emailVerified: '이메일이 성공적으로 인증되었습니다.',
enableAPIKey: 'API 키 활성화',
@@ -295,6 +296,7 @@ export const koTranslations: DefaultTranslationsObject = {
updating: '업데이트 중',
uploading: '업로드 중',
user: '사용자',
username: '사용자 이름',
users: '사용자',
value: '값',
welcome: '환영합니다',
@@ -358,6 +360,8 @@ export const koTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: '이 입력란은 두 개의 숫자가 필요합니다.',
shorterThanMax: '이 값은 최대 길이인 {{maxLength}}자보다 짧아야 합니다.',
trueOrFalse: '이 입력란은 true 또는 false만 가능합니다.',
username:
'유효한 사용자 이름을 입력해 주세요. 글자, 숫자, 하이픈, 마침표, 및 밑줄을 사용할 수 있습니다.',
validUploadID: '이 입력란은 유효한 업로드 ID가 아닙니다.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const myTranslations: DefaultTranslationsObject = {
confirmPassword: 'စကားဝှက်အား ထပ်မံ ရိုက်ထည့်ပါ။',
createFirstUser: 'ပထမဆုံး အသုံးပြုသူကို ဖန်တီးပါ။',
emailNotValid: 'ထည့်သွင်းထားသော အီးမေလ်မှာ မှားယွင်းနေပါသည်။',
emailOrUsername: 'E-mel atau Nama Pengguna',
emailSent: 'မေးလ် ပို့ထားပါသည်။',
emailVerified: 'အီးမေးလ်အတည်ပြုခဲ့ပါပြီ။',
enableAPIKey: 'API Key ကိုဖွင့်ရန်',
@@ -299,6 +300,7 @@ export const myTranslations: DefaultTranslationsObject = {
updating: 'ပြင်ဆင်ရန်',
uploading: 'တင်ပေးနေသည်',
user: 'အသုံးပြုသူ',
username: 'Nama pengguna',
users: 'အသုံးပြုသူများ',
value: 'တန်ဖိုး',
welcome: 'ကြိုဆိုပါတယ်။',
@@ -366,6 +368,8 @@ export const myTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'ဤအကွက်သည် နံပါတ်နှစ်ခု လိုအပ်ပါသည်။',
shorterThanMax: 'ဤတန်ဖိုးသည် စာလုံး {{maxLength}} လုံး၏ အမြင့်ဆုံးအရှည်ထက် ပိုတိုရပါမည်။',
trueOrFalse: 'ဤအကွက်သည် တစ်ခုခုဖြစ်ရပါမည်။',
username:
'Sila masukkan nama pengguna yang sah. Boleh mengandungi huruf, nombor, tanda hubung, titik dan garis bawah.',
validUploadID: "'ဤအကွက်သည် မှန်ကန်သော အပ်လုဒ် ID မဟုတ်ပါ။'",
},
version: {

View File

@@ -18,6 +18,7 @@ export const nbTranslations: DefaultTranslationsObject = {
confirmPassword: 'Bekreft passord',
createFirstUser: 'Opprett første bruker',
emailNotValid: 'E-posten er ikke gyldig',
emailOrUsername: 'E-post eller brukernavn',
emailSent: 'E-post sendt',
emailVerified: 'E-post bekreftet med hell.',
enableAPIKey: 'Aktiver API-nøkkel',
@@ -296,6 +297,7 @@ export const nbTranslations: DefaultTranslationsObject = {
updating: 'Oppdatering',
uploading: 'Opplasting',
user: 'Bruker',
username: 'Brukernavn',
users: 'Brukere',
value: 'Verdi',
welcome: 'Velkommen',
@@ -360,6 +362,8 @@ export const nbTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Dette feltet krever to tall.',
shorterThanMax: 'Denne verdien må være kortere enn maksimal lengde på {{maxLength}} tegn.',
trueOrFalse: 'Dette feltet kan bare være likt true eller false.',
username:
'Vennligst oppgi et gyldig brukernavn. Kan inneholde bokstaver, nummer, bindestreker, punktum og understrek.',
validUploadID: 'Dette feltet er ikke en gyldig opplastings-ID.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const nlTranslations: DefaultTranslationsObject = {
confirmPassword: 'Wachtwoord bevestigen',
createFirstUser: 'Eerste gebruiker aanmaken',
emailNotValid: 'Het ingevoerde e-mailadres is niet geldig',
emailOrUsername: 'E-mail of Gebruikersnaam',
emailSent: 'E-mail verzonden',
emailVerified: 'E-mail succesvol geverifieerd.',
enableAPIKey: 'Activeer API-sleutel',
@@ -298,6 +299,7 @@ export const nlTranslations: DefaultTranslationsObject = {
updating: 'Bijwerken',
uploading: 'Uploaden',
user: 'Gebruiker',
username: 'Gebruikersnaam',
users: 'Gebruikers',
value: 'Waarde',
welcome: 'Welkom',
@@ -362,6 +364,8 @@ export const nlTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Dit veld vereist twee nummers.',
shorterThanMax: 'Dit veld moet korter zijn dan de maximale lengte van {{maxLength}} tekens.',
trueOrFalse: 'Dit veld kan alleen waar of onwaar zijn.',
username:
'Voer een geldige gebruikersnaam in. Kan letters, cijfers, koppeltekens, punten en underscores bevatten.',
validUploadID: 'Dit veld is geen geldige upload-ID.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const plTranslations: DefaultTranslationsObject = {
confirmPassword: 'Potwierdź hasło',
createFirstUser: 'Utwórz pierwszego użytkownika',
emailNotValid: 'Podany email jest nieprawidłowy',
emailOrUsername: 'Email lub Nazwa użytkownika',
emailSent: 'Wysłano email',
emailVerified: 'Email zweryfikowany pomyślnie.',
enableAPIKey: 'Aktywuj klucz API',
@@ -296,6 +297,7 @@ export const plTranslations: DefaultTranslationsObject = {
updating: 'Aktualizacja',
uploading: 'Przesyłanie',
user: 'użytkownik',
username: 'Nazwa użytkownika',
users: 'użytkownicy',
value: 'Wartość',
welcome: 'Witaj',
@@ -360,6 +362,8 @@ export const plTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'To pole wymaga dwóch liczb.',
shorterThanMax: 'Ta wartość musi być krótsza niż maksymalna długość znaków: {{maxLength}}.',
trueOrFalse: "To pole może mieć wartość tylko 'true' lub 'false'.",
username:
'Proszę wprowadzić prawidłową nazwę użytkownika. Może zawierać litery, cyfry, myślniki, kropki i podkreślniki.',
validUploadID: 'To pole nie jest prawidłowym identyfikatorem przesyłania.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const ptTranslations: DefaultTranslationsObject = {
confirmPassword: 'Confirmar Senha',
createFirstUser: 'Criar primeiro usuário',
emailNotValid: 'O email fornecido não é válido',
emailOrUsername: 'Email ou Nome de Usuário',
emailSent: 'Email Enviado',
emailVerified: 'Email verificado com sucesso.',
enableAPIKey: 'Habilitar Chave API',
@@ -297,6 +298,7 @@ export const ptTranslations: DefaultTranslationsObject = {
updating: 'Atualizando',
uploading: 'Fazendo upload',
user: 'usuário',
username: 'Nome de usuário',
users: 'usuários',
value: 'Valor',
welcome: 'Boas vindas',
@@ -361,6 +363,8 @@ export const ptTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Esse campo requer dois números.',
shorterThanMax: 'Esse valor deve ser menor do que o máximo de {{maxLength}} caracteres.',
trueOrFalse: 'Esse campo pode ser apenas verdadeiro (true) ou falso (false)',
username:
'Por favor, insira um nome de usuário válido. Pode conter letras, números, hifens, pontos e sublinhados.',
validUploadID: "'Esse campo não é um ID de upload válido.'",
},
version: {

View File

@@ -18,6 +18,7 @@ export const roTranslations: DefaultTranslationsObject = {
confirmPassword: 'Confirmați parola',
createFirstUser: 'Creați primul utilizator',
emailNotValid: 'Emailul furnizat nu este valid',
emailOrUsername: 'Email sau Nume de utilizator',
emailSent: 'Email trimis',
emailVerified: 'E-mail verificat cu succes.',
enableAPIKey: 'Activați cheia API',
@@ -300,6 +301,7 @@ export const roTranslations: DefaultTranslationsObject = {
updating: 'Actualizare',
uploading: 'Încărcare',
user: 'Utilizator',
username: 'Nume de utilizator',
users: 'Utilizatori',
value: 'Valoare',
welcome: 'Bine ați venit',
@@ -368,6 +370,8 @@ export const roTranslations: DefaultTranslationsObject = {
shorterThanMax:
'Această valoare trebuie să fie mai scurtă decât lungimea maximă de {{maxLength}} caractere.',
trueOrFalse: 'Acest câmp poate fi doar egal cu true sau false.',
username:
'Vă rugăm să introduceți un nume de utilizator valid. Poate conține litere, numere, cratime, puncte și sublinieri.',
validUploadID: 'Acest câmp nu este un ID de încărcare valid.',
},
version: {

View File

@@ -17,6 +17,7 @@ export const rsTranslations: DefaultTranslationsObject = {
confirmPassword: 'Потврди лозинку',
createFirstUser: 'Креирај првог корисника',
emailNotValid: 'Адреса е-поште није валидна',
emailOrUsername: 'Email ili Korisničko ime',
emailSent: 'Порука е-поште прослеђена',
emailVerified: 'Uspešno verifikovan email.',
enableAPIKey: 'Омогући API кључ',
@@ -295,6 +296,7 @@ export const rsTranslations: DefaultTranslationsObject = {
updating: 'Ажурирање',
uploading: 'Пренос',
user: 'Корисник',
username: 'Korisničko ime',
users: 'Корисници',
value: 'Вредност',
welcome: 'Добродошли',
@@ -359,6 +361,8 @@ export const rsTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Ово поље захтева два броја.',
shorterThanMax: 'Ова вредност мора бити краћа од максималне дужине од {{maxLength}} карактера',
trueOrFalse: 'Ово поље може бити само тачно или нетачно',
username:
'Molimo unesite važeće korisničko ime. Može sadržati slova, brojeve, crtice, tačke i donje crte.',
validUploadID: 'Ово поље не садржи валидан ИД преноса.',
},
version: {

View File

@@ -17,6 +17,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
confirmPassword: 'Potvrdi lozinku',
createFirstUser: 'Kreiraj prvog korisnika',
emailNotValid: 'Adresa e-pošte nije validna',
emailOrUsername: 'Email ili Korisničko ime',
emailSent: 'Poruka e-pošte prosleđena',
emailVerified: 'E-pošta je uspešno verifikovana.',
enableAPIKey: 'Omogući API ključ',
@@ -295,6 +296,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
updating: 'Ažuriranje',
uploading: 'Prenos',
user: 'Korisnik',
username: 'Korisničko ime',
users: 'Korisnici',
value: 'Vrednost',
welcome: 'Dobrodošli',
@@ -359,6 +361,8 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Ovo polje zahteva dva broja.',
shorterThanMax: 'Ova vrednost mora biti kraća od maksimalne dužine od {{maxLength}} karaktera',
trueOrFalse: 'Ovo polje može biti samo tačno ili netačno',
username:
'Molimo unesite važeće korisničko ime. Može sadržavati slova, brojeve, crtice, tačke i donje crte.',
validUploadID: 'Ovo polje ne sadrži validan ID prenosa.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const ruTranslations: DefaultTranslationsObject = {
confirmPassword: 'Подтверждение пароля',
createFirstUser: 'Создание первого пользователя',
emailNotValid: 'Указанный адрес электронной почты неверен',
emailOrUsername: 'Электронная почта или Имя пользователя',
emailSent: 'Email отправлен',
emailVerified: 'Электронная почта успешно подтверждена.',
enableAPIKey: 'Активировать API ключ',
@@ -299,6 +300,7 @@ export const ruTranslations: DefaultTranslationsObject = {
updating: 'Обновление',
uploading: 'Загрузка',
user: 'пользователь',
username: 'Имя пользователя',
users: 'пользователи',
value: 'Значение',
welcome: 'Добро пожаловать',
@@ -363,6 +365,8 @@ export const ruTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'В этом поле требуется два числа.',
shorterThanMax: 'Это значение должно быть короче максимальной длины символов {{maxLength}}.',
trueOrFalse: 'Это поле может быть равно только true или false.',
username:
'Пожалуйста, введите действительное имя пользователя. Может содержать буквы, цифры, дефисы, точки и подчёркивания.',
validUploadID: "'Это поле не является действительным ID загрузки.'",
},
version: {

View File

@@ -18,6 +18,7 @@ export const skTranslations: DefaultTranslationsObject = {
confirmPassword: 'Potvrdiť heslo',
createFirstUser: 'Vytvorenie prvého používateľa',
emailNotValid: 'Zadaný e-mail nie je platný',
emailOrUsername: 'E-mail alebo Užívateľské meno',
emailSent: 'E-mail bol odoslaný',
emailVerified: 'Email úspešne overený.',
enableAPIKey: 'Povolenie API kľúča',
@@ -297,6 +298,7 @@ export const skTranslations: DefaultTranslationsObject = {
updating: 'Aktualizácia',
uploading: 'Nahrávanie',
user: 'Používateľ',
username: 'Používateľské meno',
users: 'Používatelia',
value: 'Hodnota',
welcome: 'Vitajte',
@@ -361,6 +363,8 @@ export const skTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Toto pole vyžaduje dve čísla.',
shorterThanMax: 'Táto hodnota musí byť kratšia ako maximálna dĺžka {{maxLength}} znakov.',
trueOrFalse: 'Toto pole môže byť rovné iba true alebo false.',
username:
'Prosím, zadajte platné používateľské meno. Môže obsahovať písmená, čísla, pomlčky, bodky a podčiarknutia.',
validUploadID: 'Toto pole nie je platné ID pre odoslanie.',
},
version: {

View File

@@ -18,6 +18,7 @@ export const svTranslations: DefaultTranslationsObject = {
confirmPassword: 'Bekräfta Lösenord',
createFirstUser: 'Skapa första användaren',
emailNotValid: 'Angiven e-postadress är inte giltig',
emailOrUsername: 'E-post eller användarnamn',
emailSent: 'E-posten Skickad',
emailVerified: 'E-post verifierad framgångsrikt.',
enableAPIKey: 'Aktivera API nyckel',
@@ -296,6 +297,7 @@ export const svTranslations: DefaultTranslationsObject = {
updating: 'Uppdatering',
uploading: 'Uppladdning',
user: 'Användare',
username: 'Användarnamn',
users: 'Användare',
value: 'Värde',
welcome: 'Välkommen',
@@ -360,6 +362,8 @@ export const svTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Detta fält kräver två nummer.',
shorterThanMax: 'Detta värde måste vara kortare än maxlängden på {{maxLength}} tecken.',
trueOrFalse: 'Detta fält kan bara vara lika med sant eller falskt.',
username:
'Var god ange ett giltigt användarnamn. Kan innehålla bokstäver, siffror, bindestreck, punkter och understreck.',
validUploadID: 'Det här fältet är inte ett giltigt uppladdnings-ID',
},
version: {

View File

@@ -18,6 +18,7 @@ export const thTranslations: DefaultTranslationsObject = {
confirmPassword: 'ยืนยันรหัสผ่าน',
createFirstUser: 'สร้างผู้ใช้แรก',
emailNotValid: 'อีเมลไม่ถูกต้อง',
emailOrUsername: 'อีเมลหรือชื่อผู้ใช้',
emailSent: 'ส่งอีเมลเรียบร้อยแล้ว',
emailVerified: 'อีเมลได้รับการยืนยันเรียบร้อยแล้ว',
enableAPIKey: 'เปิดใช้ API Key',
@@ -292,6 +293,7 @@ export const thTranslations: DefaultTranslationsObject = {
updating: 'กำลังอัปเดต',
uploading: 'กำลังอัปโหลด',
user: 'ผู้ใช้',
username: 'ชื่อผู้ใช้',
users: 'ผู้ใช้',
value: 'ค่า',
welcome: 'ยินดีต้อนรับ',
@@ -354,6 +356,7 @@ export const thTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'ต้องมีตัวเลข 2 ค่า',
shorterThanMax: 'ค่าต้องมีความยาวน้อยกว่า {{maxLength}} ตัวอักษร',
trueOrFalse: 'เป็นได้แค่ "ใช่" หรือ "ไม่ใช่"',
username: 'กรุณาใส่ชื่อผู้ใช้ที่ถูกต้อง สามารถมีตัวอักษร ตัวเลข ขีดกลาง จุด และขีดล่าง',
validUploadID: 'ไม่ใช่ ID ของการอัปโหลดที่ถูกต้อง',
},
version: {

View File

@@ -18,6 +18,7 @@ export const trTranslations: DefaultTranslationsObject = {
confirmPassword: 'Parolayı Onayla',
createFirstUser: 'İlk kullanıcı oluştur',
emailNotValid: 'Girilen e-posta geçersiz',
emailOrUsername: 'E-posta veya Kullanıcı Adı',
emailSent: 'E-posta gönderildi',
emailVerified: 'E-posta başarıyla doğrulandı.',
enableAPIKey: 'Api anahtarını etkinleştir',
@@ -300,6 +301,7 @@ export const trTranslations: DefaultTranslationsObject = {
updating: 'Güncelleniyor',
uploading: 'Yükleniyor',
user: 'kullanıcı',
username: 'Kullanıcı Adı',
users: 'kullanıcı',
value: 'Değer',
welcome: 'Hoşgeldiniz',
@@ -364,6 +366,8 @@ export const trTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Bu alana en az iki rakam girilmesi zorunludur.',
shorterThanMax: 'Bu alan {{maxLength}} karakterden daha kısa olmalıdır.',
trueOrFalse: 'Bu alan yalnızca doğru ve yanlış olabilir.',
username:
'Lütfen geçerli bir kullanıcı adı girin. Harfler, numaralar, kısa çizgiler, noktalar ve alt çizgiler içerebilir.',
validUploadID: "'Bu alan geçerli bir karşıya yükleme ID'sine sahip değil.'",
},
version: {

View File

@@ -18,6 +18,7 @@ export const ukTranslations: DefaultTranslationsObject = {
confirmPassword: 'Підтвердження паролю',
createFirstUser: 'Створення першого користувача',
emailNotValid: 'Вказана адреса електронної пошти недійсна',
emailOrUsername: "Електронна пошта або Ім'я користувача",
emailSent: 'Лист відправлено',
emailVerified: 'Електронну пошту успішно підтверджено.',
enableAPIKey: 'Активувати API ключ',
@@ -296,6 +297,7 @@ export const ukTranslations: DefaultTranslationsObject = {
updating: 'оновлення',
uploading: 'завантаження',
user: 'Користувач',
username: "Ім'я користувача",
users: 'Користувачі',
value: 'Значення',
welcome: 'Вітаю',
@@ -360,6 +362,8 @@ export const ukTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'У цьому полі потрібно ввести два числа.',
shorterThanMax: 'Це значення має дорівнювати або бути коротшим, ніж {{maxLength}} символів.',
trueOrFalse: 'Це поле може мати значення тільки true або false.',
username:
"Будь ласка, введіть дійсне ім'я користувача. Може містити літери, цифри, дефіси, крапки та підкреслення.",
validUploadID: 'Це поле не є дійсним ID завантаження.',
},
version: {

View File

@@ -17,6 +17,7 @@ export const viTranslations: DefaultTranslationsObject = {
confirmPassword: 'Xác nhận mật khẩu',
createFirstUser: 'Tạo người dùng đầu tiên',
emailNotValid: 'Email không chính xác',
emailOrUsername: 'Email hoặc Tên tài khoản',
emailSent: 'Email đã được gửi',
emailVerified: 'Email đã được xác minh thành công.',
enableAPIKey: 'Kích hoạt API Key',
@@ -294,6 +295,7 @@ export const viTranslations: DefaultTranslationsObject = {
updating: 'Đang cập nhật',
uploading: 'Đang tải lên',
user: 'Người dùng',
username: 'Tên đăng nhập',
users: 'Người dùng',
value: 'Giá trị',
welcome: 'Xin chào',
@@ -358,6 +360,8 @@ export const viTranslations: DefaultTranslationsObject = {
requiresTwoNumbers: 'Field này cần tối thiểu 2 chữ số.',
shorterThanMax: 'Giá trị phải ngắn hơn hoặc bằng {{maxLength}} ký tự.',
trueOrFalse: 'Field này chỉ có thể chứa giá trị true hoặc false.',
username:
'Vui lòng nhập một tên người dùng hợp lệ. Có thể chứa các chữ cái, số, dấu gạch ngang, dấu chấm và dấu gạch dưới.',
validUploadID: "'Field này không chứa ID tải lên hợp lệ.'",
},
version: {

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