chore: dynamically loads date-fns locales
This commit is contained in:
@@ -33,23 +33,30 @@ export const RootLayout = async ({
|
||||
const headers = getHeaders()
|
||||
const cookies = parseCookies(headers)
|
||||
|
||||
const lang =
|
||||
const languageCode =
|
||||
getRequestLanguage({
|
||||
config,
|
||||
cookies,
|
||||
headers,
|
||||
}) ?? config.i18n.fallbackLanguage
|
||||
|
||||
const i18n = initI18n({ config: config.i18n, context: 'client', language: lang })
|
||||
const i18n = await initI18n({ config: config.i18n, context: 'client', language: languageCode })
|
||||
const clientConfig = await createClientConfig({ config, t: i18n.t })
|
||||
|
||||
const dir = rtlLanguages.includes(lang) ? 'RTL' : 'LTR'
|
||||
const dir = rtlLanguages.includes(languageCode) ? 'RTL' : 'LTR'
|
||||
|
||||
const languageOptions = Object.entries(i18n.translations || {}).map(
|
||||
([language, translations]) => ({
|
||||
label: translations.general.thisLanguage,
|
||||
const languageOptions = Object.entries(config.i18n.supportedLanguages || {}).reduce(
|
||||
(acc, [language, languageConfig]) => {
|
||||
if (Object.keys(config.i18n.supportedLanguages).includes(language)) {
|
||||
acc.push({
|
||||
label: languageConfig.translations.general.thisLanguage,
|
||||
value: language,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -71,17 +78,18 @@ export const RootLayout = async ({
|
||||
})
|
||||
|
||||
return (
|
||||
<html dir={dir} lang={lang}>
|
||||
<html dir={dir} lang={languageCode}>
|
||||
<body>
|
||||
<RootProvider
|
||||
componentMap={componentMap}
|
||||
config={clientConfig}
|
||||
dateFNSKey={i18n.dateFNSKey}
|
||||
fallbackLang={clientConfig.i18n.fallbackLanguage}
|
||||
lang={lang}
|
||||
languageCode={languageCode}
|
||||
languageOptions={languageOptions}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
switchLanguageServerAction={switchLanguageServerAction}
|
||||
translations={i18n.translations[lang]}
|
||||
translations={i18n.translations}
|
||||
>
|
||||
{wrappedChildren}
|
||||
</RootProvider>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import type { BuildFormStateArgs } from '@payloadcms/ui/forms/buildStateFromSchema'
|
||||
import type {
|
||||
DocumentPreferences,
|
||||
Field,
|
||||
PayloadRequest,
|
||||
SanitizedConfig,
|
||||
TypeWithID,
|
||||
} from 'payload/types'
|
||||
import type { DocumentPreferences, Field, PayloadRequest, TypeWithID } from 'payload/types'
|
||||
|
||||
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
|
||||
import { reduceFieldsToValues } from '@payloadcms/ui/utilities/reduceFieldsToValues'
|
||||
@@ -15,32 +9,21 @@ import type { FieldSchemaMap } from '../../utilities/buildFieldSchemaMap/types.j
|
||||
|
||||
import { buildFieldSchemaMap } from '../../utilities/buildFieldSchemaMap/index.js'
|
||||
|
||||
let cached: {
|
||||
promise: Promise<FieldSchemaMap> | null
|
||||
schemaMap: FieldSchemaMap | null
|
||||
} = global._payload_fieldSchemaMap
|
||||
let cached = global._payload_fieldSchemaMap
|
||||
|
||||
if (!cached) {
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
cached = global._payload_fieldSchemaMap = { promise: null, schemaMap: null }
|
||||
cached = global._payload_fieldSchemaMap = null
|
||||
}
|
||||
|
||||
export const getFieldSchemaMap = async (config: SanitizedConfig): Promise<FieldSchemaMap> => {
|
||||
if (cached.schemaMap && process.env.NODE_ENV !== 'development') {
|
||||
return cached.schemaMap
|
||||
export const getFieldSchemaMap = (req: PayloadRequest): FieldSchemaMap => {
|
||||
if (cached && process.env.NODE_ENV !== 'development') {
|
||||
return cached
|
||||
}
|
||||
|
||||
if (!cached.promise) {
|
||||
cached.promise = buildFieldSchemaMap(config)
|
||||
}
|
||||
cached = buildFieldSchemaMap(req)
|
||||
|
||||
try {
|
||||
cached.schemaMap = await cached.promise
|
||||
} catch (e) {
|
||||
cached.promise = null
|
||||
throw e
|
||||
}
|
||||
|
||||
return cached.schemaMap
|
||||
return cached
|
||||
}
|
||||
|
||||
export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
|
||||
@@ -76,7 +59,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
|
||||
})
|
||||
}
|
||||
|
||||
const fieldSchemaMap = await getFieldSchemaMap(req.payload.config)
|
||||
const fieldSchemaMap = getFieldSchemaMap(req)
|
||||
|
||||
const id = collectionSlug ? reqData.id : undefined
|
||||
const schemaPathSegments = schemaPath.split('.')
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
import type { SanitizedConfig } from 'payload/types'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
|
||||
import type { FieldSchemaMap } from './types.js'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
export const buildFieldSchemaMap = async (config: SanitizedConfig): Promise<FieldSchemaMap> => {
|
||||
export const buildFieldSchemaMap = ({
|
||||
i18n,
|
||||
payload: { config },
|
||||
}: PayloadRequest): FieldSchemaMap => {
|
||||
const result: FieldSchemaMap = new Map()
|
||||
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
await Promise.all(
|
||||
config.collections.map(async (collection) => {
|
||||
await traverseFields({
|
||||
config.collections.forEach((collection) => {
|
||||
traverseFields({
|
||||
config,
|
||||
fields: collection.fields,
|
||||
i18n,
|
||||
schemaMap: result,
|
||||
schemaPath: collection.slug,
|
||||
validRelationships,
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
config.globals.map(async (global) => {
|
||||
await traverseFields({
|
||||
config.globals.forEach((global) => {
|
||||
traverseFields({
|
||||
config,
|
||||
fields: global.fields,
|
||||
i18n,
|
||||
schemaMap: result,
|
||||
schemaPath: global.slug,
|
||||
validRelationships,
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { Field, SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { tabHasName } from 'payload/types'
|
||||
@@ -7,28 +8,30 @@ import type { FieldSchemaMap } from './types.js'
|
||||
type Args = {
|
||||
config: SanitizedConfig
|
||||
fields: Field[]
|
||||
i18n: I18n
|
||||
schemaMap: FieldSchemaMap
|
||||
schemaPath: string
|
||||
validRelationships: string[]
|
||||
}
|
||||
|
||||
export const traverseFields = async ({
|
||||
export const traverseFields = ({
|
||||
config,
|
||||
fields,
|
||||
i18n,
|
||||
schemaMap,
|
||||
schemaPath,
|
||||
validRelationships,
|
||||
}: Args): Promise<void> => {
|
||||
await Promise.all(
|
||||
fields.map(async (field) => {
|
||||
}: Args) => {
|
||||
fields.map((field) => {
|
||||
switch (field.type) {
|
||||
case 'group':
|
||||
case 'array':
|
||||
schemaMap.set(`${schemaPath}.${field.name}`, field.fields)
|
||||
|
||||
await traverseFields({
|
||||
traverseFields({
|
||||
config,
|
||||
fields: field.fields,
|
||||
i18n,
|
||||
schemaMap,
|
||||
schemaPath: `${schemaPath}.${field.name}`,
|
||||
validRelationships,
|
||||
@@ -37,9 +40,10 @@ export const traverseFields = async ({
|
||||
|
||||
case 'collapsible':
|
||||
case 'row':
|
||||
await traverseFields({
|
||||
traverseFields({
|
||||
config,
|
||||
fields: field.fields,
|
||||
i18n,
|
||||
schemaMap,
|
||||
schemaPath,
|
||||
validRelationships,
|
||||
@@ -47,27 +51,27 @@ export const traverseFields = async ({
|
||||
break
|
||||
|
||||
case 'blocks':
|
||||
await Promise.all(
|
||||
field.blocks.map(async (block) => {
|
||||
field.blocks.map((block) => {
|
||||
const blockSchemaPath = `${schemaPath}.${field.name}.${block.slug}`
|
||||
|
||||
schemaMap.set(blockSchemaPath, block.fields)
|
||||
|
||||
await traverseFields({
|
||||
traverseFields({
|
||||
config,
|
||||
fields: block.fields,
|
||||
i18n,
|
||||
schemaMap,
|
||||
schemaPath: blockSchemaPath,
|
||||
validRelationships,
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
break
|
||||
|
||||
case 'richText':
|
||||
if (typeof field.editor.generateSchemaMap === 'function') {
|
||||
await field.editor.generateSchemaMap({
|
||||
field.editor.generateSchemaMap({
|
||||
config,
|
||||
i18n,
|
||||
schemaMap,
|
||||
schemaPath: `${schemaPath}.${field.name}`,
|
||||
})
|
||||
@@ -76,25 +80,23 @@ export const traverseFields = async ({
|
||||
break
|
||||
|
||||
case 'tabs':
|
||||
await Promise.all(
|
||||
field.tabs.map(async (tab) => {
|
||||
field.tabs.map((tab) => {
|
||||
const tabSchemaPath = tabHasName(tab) ? `${schemaPath}.${tab.name}` : schemaPath
|
||||
|
||||
if (tabHasName(tab)) {
|
||||
schemaMap.set(tabSchemaPath, tab.fields)
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
traverseFields({
|
||||
config,
|
||||
fields: tab.fields,
|
||||
i18n,
|
||||
schemaMap,
|
||||
schemaPath: tabSchemaPath,
|
||||
validRelationships,
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
break
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export const createPayloadRequest = async ({
|
||||
headers: request.headers,
|
||||
})
|
||||
|
||||
const i18n = initI18n({
|
||||
const i18n = await initI18n({
|
||||
config: config.i18n,
|
||||
context: 'api',
|
||||
language,
|
||||
|
||||
@@ -6,13 +6,13 @@ import { cookies, headers } from 'next/headers.js'
|
||||
|
||||
import { getRequestLanguage } from './getRequestLanguage.js'
|
||||
|
||||
export const getNextI18n = ({
|
||||
export const getNextI18n = async ({
|
||||
config,
|
||||
language,
|
||||
}: {
|
||||
config: SanitizedConfig
|
||||
language?: string
|
||||
}): I18n =>
|
||||
}): Promise<I18n> =>
|
||||
initI18n({
|
||||
config: config.i18n,
|
||||
context: 'client',
|
||||
|
||||
@@ -44,7 +44,7 @@ export const initPage = async ({
|
||||
const cookies = parseCookies(headers)
|
||||
const language = getRequestLanguage({ config: payload.config, cookies, headers })
|
||||
|
||||
const i18n = initI18n({
|
||||
const i18n = await initI18n({
|
||||
config: payload.config.i18n,
|
||||
context: 'client',
|
||||
language,
|
||||
|
||||
@@ -89,7 +89,7 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const i18n = getNextI18n({
|
||||
const i18n = await getNextI18n({
|
||||
config,
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export const generatePageMetadata = async ({
|
||||
}): Promise<Metadata> => {
|
||||
const config = await configPromise
|
||||
|
||||
const i18n = getNextI18n({
|
||||
const i18n = await getNextI18n({
|
||||
config,
|
||||
})
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export const generatePageMetadata = async ({ config: configPromise, params }: Ar
|
||||
const isGlobal = segmentOne === 'globals'
|
||||
const isCollection = segmentOne === 'collections'
|
||||
|
||||
const i18n = getNextI18n({
|
||||
const i18n = await getNextI18n({
|
||||
config,
|
||||
})
|
||||
|
||||
|
||||
@@ -85,7 +85,9 @@ export const SetStepNav: React.FC<{
|
||||
url: `${adminRoute}/collections/${collectionSlug}/${id}/versions`,
|
||||
},
|
||||
{
|
||||
label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n?.language) : '',
|
||||
label: doc?.createdAt
|
||||
? formatDate({ date: doc.createdAt, i18n, pattern: dateFormat })
|
||||
: '',
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -101,7 +103,9 @@ export const SetStepNav: React.FC<{
|
||||
url: `${adminRoute}/globals/${globalConfig.slug}/versions`,
|
||||
},
|
||||
{
|
||||
label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n?.language) : '',
|
||||
label: doc?.createdAt
|
||||
? formatDate({ date: doc.createdAt, i18n, pattern: dateFormat })
|
||||
: '',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
|
||||
} = config
|
||||
|
||||
const formattedCreatedAt = doc?.createdAt
|
||||
? formatDate(doc.createdAt, dateFormat, i18n.language)
|
||||
? formatDate({ date: doc.createdAt, i18n, pattern: dateFormat })
|
||||
: ''
|
||||
|
||||
const originalDocFetchURL = `${serverURL}${apiRoute}/${globalSlug ? 'globals/' : ''}${
|
||||
|
||||
@@ -88,7 +88,7 @@ export const SelectComparison: React.FC<Props> = (props) => {
|
||||
setOptions((existingOptions) => [
|
||||
...existingOptions,
|
||||
...data.docs.map((doc) => ({
|
||||
label: formatDate(doc.updatedAt, dateFormat, i18n.language),
|
||||
label: formatDate({ date: doc.updatedAt, i18n, pattern: dateFormat }),
|
||||
value: doc.id,
|
||||
})),
|
||||
])
|
||||
|
||||
@@ -22,7 +22,7 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
const doc: any = {} // TODO: figure this out
|
||||
|
||||
const formattedCreatedAt = doc?.createdAt
|
||||
? formatDate(doc.createdAt, config?.admin?.dateFormat, i18n?.language)
|
||||
? formatDate({ date: doc.createdAt, i18n, pattern: config?.admin?.dateFormat })
|
||||
: ''
|
||||
|
||||
if (collectionConfig) {
|
||||
|
||||
@@ -38,7 +38,8 @@ export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
||||
|
||||
return (
|
||||
<Link href={to}>
|
||||
{cellData && formatDate(cellData as Date | number | string, dateFormat, i18n.language)}
|
||||
{cellData &&
|
||||
formatDate({ date: cellData as Date | number | string, i18n, pattern: dateFormat })}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,9 +34,10 @@ type RichTextAdapterBase<
|
||||
}) => Map<string, React.ReactNode>
|
||||
generateSchemaMap?: (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18n
|
||||
schemaMap: Map<string, Field[]>
|
||||
schemaPath: string
|
||||
}) => Map<string, Field[]> | Promise<Map<string, Field[]>>
|
||||
}) => Map<string, Field[]>
|
||||
outputSchema?: ({
|
||||
collectionIDFieldTypes,
|
||||
config,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Translations } from '@payloadcms/translations'
|
||||
import type { SupportedLanguages } from '@payloadcms/translations'
|
||||
|
||||
import type { Permissions } from '../../auth/index.js'
|
||||
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||
@@ -43,7 +43,7 @@ export type InitPageResult = {
|
||||
locale: Locale
|
||||
permissions: Permissions
|
||||
req: PayloadRequest
|
||||
translations: Translations
|
||||
translations: SupportedLanguages
|
||||
visibleEntities: VisibleEntities
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import en from '@payloadcms/translations/languages/en'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
import type {
|
||||
|
||||
@@ -673,11 +673,12 @@ export type Config = {
|
||||
|
||||
export type SanitizedConfig = Omit<
|
||||
DeepRequired<Config>,
|
||||
'collections' | 'endpoint' | 'globals' | 'localization'
|
||||
'collections' | 'endpoint' | 'globals' | 'i18n' | 'localization'
|
||||
> & {
|
||||
collections: SanitizedCollectionConfig[]
|
||||
endpoints: Endpoint[]
|
||||
globals: SanitizedGlobalConfig[]
|
||||
i18n: Required<I18nOptions>
|
||||
localization: SanitizedLocalizationConfig | false
|
||||
paths: {
|
||||
config: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
|
||||
import en from '@payloadcms/translations/languages/en'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import APIError from './APIError.js'
|
||||
@@ -8,7 +8,7 @@ import APIError from './APIError.js'
|
||||
class AuthenticationError extends APIError {
|
||||
constructor(t?: TFunction) {
|
||||
super(
|
||||
t ? t('error:emailOrPasswordIncorrect') : en.error.emailOrPasswordIncorrect,
|
||||
t ? t('error:emailOrPasswordIncorrect') : en.translations.error.emailOrPasswordIncorrect,
|
||||
httpStatus.UNAUTHORIZED,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
|
||||
import en from '@payloadcms/translations/languages/en'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import APIError from './APIError.js'
|
||||
|
||||
class ErrorDeletingFile extends APIError {
|
||||
constructor(t?: TFunction) {
|
||||
super(t ? t('error:deletingFile') : en.error.deletingFile, httpStatus.INTERNAL_SERVER_ERROR)
|
||||
super(
|
||||
t ? t('error:deletingFile') : en.translations.error.deletingFile,
|
||||
httpStatus.INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
|
||||
import en from '@payloadcms/translations/languages/en'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import APIError from './APIError.js'
|
||||
@@ -8,7 +8,7 @@ import APIError from './APIError.js'
|
||||
class FileUploadError extends APIError {
|
||||
constructor(t?: TFunction) {
|
||||
super(
|
||||
t ? t('error:problemUploadingFile') : en.error.problemUploadingFile,
|
||||
t ? t('error:problemUploadingFile') : en.translations.error.problemUploadingFile,
|
||||
httpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
|
||||
import en from '@payloadcms/translations/languages/en'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import APIError from './APIError.js'
|
||||
@@ -8,7 +8,7 @@ import APIError from './APIError.js'
|
||||
class Forbidden extends APIError {
|
||||
constructor(t?: TFunction) {
|
||||
super(
|
||||
t ? t('error:notAllowedToPerformAction') : en.error.notAllowedToPerformAction,
|
||||
t ? t('error:notAllowedToPerformAction') : en.translations.error.notAllowedToPerformAction,
|
||||
httpStatus.FORBIDDEN,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
|
||||
import en from '@payloadcms/translations/languages/en'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import APIError from './APIError.js'
|
||||
|
||||
class LockedAuth extends APIError {
|
||||
constructor(t?: TFunction) {
|
||||
super(t ? t('error:userLocked') : en.error.userLocked, httpStatus.UNAUTHORIZED)
|
||||
super(t ? t('error:userLocked') : en.translations.error.userLocked, httpStatus.UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
|
||||
import en from '@payloadcms/translations/languages/en'
|
||||
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import APIError from './APIError.js'
|
||||
|
||||
class MissingFile extends APIError {
|
||||
constructor(t?: TFunction) {
|
||||
super(t ? t('error:noFilesUploaded') : en.error.noFilesUploaded, httpStatus.BAD_REQUEST)
|
||||
super(
|
||||
t ? t('error:noFilesUploaded') : en.translations.error.noFilesUploaded,
|
||||
httpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
|
||||
import en from '@payloadcms/translations/languages/en'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import APIError from './APIError.js'
|
||||
|
||||
class NotFound extends APIError {
|
||||
constructor(t?: TFunction) {
|
||||
super(t ? t('general:notFound') : en.general.notFound, httpStatus.NOT_FOUND)
|
||||
super(t ? t('general:notFound') : en.translations.general.notFound, httpStatus.NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
|
||||
import en from '@payloadcms/translations/languages/en'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import APIError from './APIError.js'
|
||||
|
||||
class UnauthorizedError extends APIError {
|
||||
constructor(t?: TFunction) {
|
||||
super(t ? t('error:unauthorized') : en.error.unauthorized, httpStatus.UNAUTHORIZED)
|
||||
super(t ? t('error:unauthorized') : en.translations.error.unauthorized, httpStatus.UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
|
||||
import en from '@payloadcms/translations/languages/en'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import APIError from './APIError.js'
|
||||
@@ -10,8 +10,8 @@ class ValidationError extends APIError<{ field: string; message: string }[]> {
|
||||
const message = t
|
||||
? t('error:followingFieldsInvalid', { count: results.length })
|
||||
: results.length === 1
|
||||
? en.error.followingFieldsInvalid_one
|
||||
: en.error.followingFieldsInvalid_other
|
||||
? en.translations.error.followingFieldsInvalid_one
|
||||
: en.translations.error.followingFieldsInvalid_other
|
||||
|
||||
super(`${message} ${results.map((f) => f.field).join(', ')}`, httpStatus.BAD_REQUEST, results)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { I18n } from '@payloadcms/translations'
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { FieldWithRichTextRequiredEditor } from 'payload/types'
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
|
||||
import type { HTMLConverter } from '../converters/html/converter/types.js'
|
||||
import type { FeatureProviderProviderServer } from '../types.js'
|
||||
import type { ClientProps } from './feature.client.js'
|
||||
@@ -66,9 +64,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
||||
disabledCollections: props.disabledCollections,
|
||||
enabledCollections: props.enabledCollections,
|
||||
} as ExclusiveLinkCollectionsProps,
|
||||
generateSchemaMap: ({ config, props }) => {
|
||||
const i18n = initI18n({ config: config.i18n, context: 'client' })
|
||||
|
||||
generateSchemaMap: ({ config, i18n, props }) => {
|
||||
return {
|
||||
fields: transformExtraFields(
|
||||
props.fields,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Transformer } from '@lexical/markdown'
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
import type { Klass, LexicalEditor, LexicalNode, SerializedEditorState } from 'lexical'
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
@@ -178,6 +179,7 @@ export type ServerFeature<ServerProps, ClientFeatureProps> = {
|
||||
clientFeatureProps?: ClientFeatureProps
|
||||
generateComponentMap?: (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18n
|
||||
props: ServerProps
|
||||
schemaPath: string
|
||||
}) => {
|
||||
@@ -185,12 +187,17 @@ export type ServerFeature<ServerProps, ClientFeatureProps> = {
|
||||
}
|
||||
generateSchemaMap?: (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18n
|
||||
props: ServerProps
|
||||
schemaMap: Map<string, Field[]>
|
||||
schemaPath: string
|
||||
}) => {
|
||||
}) =>
|
||||
| {
|
||||
[key: string]: Field[]
|
||||
}
|
||||
| Promise<{
|
||||
[key: string]: Field[]
|
||||
}>
|
||||
generatedTypes?: {
|
||||
modifyOutputSchema: ({
|
||||
collectionIDFieldTypes,
|
||||
|
||||
@@ -39,6 +39,7 @@ export const getGenerateComponentMap =
|
||||
) {
|
||||
const components = resolvedFeature.generateComponentMap({
|
||||
config,
|
||||
i18n,
|
||||
props: resolvedFeature.serverFeatureProps,
|
||||
schemaPath,
|
||||
})
|
||||
@@ -68,6 +69,7 @@ export const getGenerateComponentMap =
|
||||
) {
|
||||
const schemas = resolvedFeature.generateSchemaMap({
|
||||
config,
|
||||
i18n,
|
||||
props: resolvedFeature.serverFeatureProps,
|
||||
schemaMap: new Map(),
|
||||
schemaPath,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { cloneDeep } from './field/lexical/utils/cloneDeep.js'
|
||||
|
||||
export const getGenerateSchemaMap =
|
||||
(args: { resolvedFeatureMap: ResolvedServerFeatureMap }): RichTextAdapter['generateSchemaMap'] =>
|
||||
({ config, schemaMap, schemaPath }) => {
|
||||
({ config, i18n, schemaMap, schemaPath }) => {
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
for (const [featureKey, resolvedFeature] of args.resolvedFeatureMap.entries()) {
|
||||
@@ -20,6 +20,7 @@ export const getGenerateSchemaMap =
|
||||
}
|
||||
const schemas = resolvedFeature.generateSchemaMap({
|
||||
config,
|
||||
i18n,
|
||||
props: resolvedFeature.serverFeatureProps,
|
||||
schemaMap,
|
||||
schemaPath,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { RichTextAdapter } from 'payload/types'
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
|
||||
import type { AdapterArguments, RichTextCustomElement } from './types.js'
|
||||
@@ -12,8 +11,7 @@ import { uploadFieldsSchemaPath } from './field/elements/upload/shared.js'
|
||||
|
||||
export const getGenerateSchemaMap =
|
||||
(args: AdapterArguments): RichTextAdapter['generateSchemaMap'] =>
|
||||
({ config, schemaMap, schemaPath }) => {
|
||||
const i18n = initI18n({ config: config.i18n, context: 'client' })
|
||||
({ config, i18n, schemaMap, schemaPath }) => {
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
|
||||
;(args?.admin?.elements || Object.values(elementTypes)).forEach((el) => {
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@swc/core": "^1.3.102",
|
||||
"@types/react": "18.2.74",
|
||||
"date-fns": "3.3.1",
|
||||
"typescript": "5.4.4"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import ar from '../languages/ar.js'
|
||||
import type { SupportedLanguages } from '../types.js'
|
||||
|
||||
import { ar } from '../languages/ar.js'
|
||||
import az from '../languages/az.js'
|
||||
import bg from '../languages/bg.js'
|
||||
import cs from '../languages/cs.js'
|
||||
import de from '../languages/de.js'
|
||||
import en from '../languages/en.js'
|
||||
import { de } from '../languages/de.js'
|
||||
import { en } from '../languages/en.js'
|
||||
import es from '../languages/es.js'
|
||||
import fa from '../languages/fa.js'
|
||||
import fr from '../languages/fr.js'
|
||||
@@ -31,35 +33,33 @@ import zhTw from '../languages/zhTw.js'
|
||||
|
||||
export const translations = {
|
||||
ar,
|
||||
az,
|
||||
bg,
|
||||
cs,
|
||||
// az,
|
||||
// bg,
|
||||
// cs,
|
||||
de,
|
||||
en,
|
||||
es,
|
||||
fa,
|
||||
fr,
|
||||
hr,
|
||||
hu,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
my,
|
||||
nb,
|
||||
nl,
|
||||
pl,
|
||||
pt,
|
||||
ro,
|
||||
rs,
|
||||
'rs-latin': rsLatin,
|
||||
ru,
|
||||
sv,
|
||||
th,
|
||||
tr,
|
||||
ua,
|
||||
vi,
|
||||
zh,
|
||||
'zh-tw': zhTw,
|
||||
} as {
|
||||
[locale: string]: Record<string, Record<string, string>>
|
||||
}
|
||||
// es,
|
||||
// fa,
|
||||
// fr,
|
||||
// hr,
|
||||
// hu,
|
||||
// it,
|
||||
// ja,
|
||||
// ko,
|
||||
// my,
|
||||
// nb,
|
||||
// nl,
|
||||
// pl,
|
||||
// pt,
|
||||
// ro,
|
||||
// rs,
|
||||
// 'rs-latin': rsLatin,
|
||||
// ru,
|
||||
// sv,
|
||||
// th,
|
||||
// tr,
|
||||
// ua,
|
||||
// vi,
|
||||
// zh,
|
||||
// 'zh-tw': zhTw,
|
||||
} as SupportedLanguages
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { importDateFNSLocale } from '../importDateFNSLocale.js'
|
||||
export { getTranslation } from '../utilities/getTranslation.js'
|
||||
export { initI18n, matchLanguage, t } from '../utilities/init.js'
|
||||
export type * from '../types.js'
|
||||
|
||||
116
packages/translations/src/importDateFNSLocale.ts
Normal file
116
packages/translations/src/importDateFNSLocale.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { Locale } from 'date-fns'
|
||||
|
||||
export const importDateFNSLocale = async (locale: string): Promise<Locale> => {
|
||||
let result
|
||||
|
||||
switch (locale) {
|
||||
case 'ar':
|
||||
result = await import('date-fns/locale/ar')
|
||||
|
||||
break
|
||||
case 'az':
|
||||
result = await import('date-fns/locale/az')
|
||||
|
||||
break
|
||||
case 'bg':
|
||||
result = await import('date-fns/locale/bg')
|
||||
|
||||
break
|
||||
case 'cs':
|
||||
result = await import('date-fns/locale/cs')
|
||||
|
||||
break
|
||||
case 'de':
|
||||
result = await import('date-fns/locale/de')
|
||||
|
||||
break
|
||||
case 'en-US':
|
||||
result = await import('date-fns/locale/en-US')
|
||||
|
||||
break
|
||||
case 'es':
|
||||
result = await import('date-fns/locale/es')
|
||||
|
||||
break
|
||||
case 'fa-IR':
|
||||
result = await import('date-fns/locale/fa-IR')
|
||||
|
||||
break
|
||||
case 'fr':
|
||||
result = await import('date-fns/locale/fr')
|
||||
|
||||
break
|
||||
case 'hr':
|
||||
result = await import('date-fns/locale/hr')
|
||||
|
||||
break
|
||||
case 'hu':
|
||||
result = await import('date-fns/locale/hu')
|
||||
|
||||
break
|
||||
case 'it':
|
||||
result = await import('date-fns/locale/it')
|
||||
|
||||
break
|
||||
case 'ja':
|
||||
result = await import('date-fns/locale/ja')
|
||||
|
||||
break
|
||||
case 'ko':
|
||||
result = await import('date-fns/locale/ko')
|
||||
|
||||
break
|
||||
case 'nb':
|
||||
result = await import('date-fns/locale/nb')
|
||||
|
||||
break
|
||||
case 'nl':
|
||||
result = await import('date-fns/locale/nl')
|
||||
|
||||
break
|
||||
case 'pl':
|
||||
result = await import('date-fns/locale/pl')
|
||||
|
||||
break
|
||||
case 'pt':
|
||||
result = await import('date-fns/locale/pt')
|
||||
|
||||
break
|
||||
case 'ro':
|
||||
result = await import('date-fns/locale/ro')
|
||||
|
||||
break
|
||||
case 'ru':
|
||||
result = await import('date-fns/locale/ru')
|
||||
|
||||
break
|
||||
case 'sv':
|
||||
result = await import('date-fns/locale/sv')
|
||||
|
||||
break
|
||||
case 'th':
|
||||
result = await import('date-fns/locale/th')
|
||||
|
||||
break
|
||||
case 'tr':
|
||||
result = await import('date-fns/locale/tr')
|
||||
|
||||
break
|
||||
case 'vi':
|
||||
result = await import('date-fns/locale/vi')
|
||||
|
||||
break
|
||||
case 'zh-CN':
|
||||
result = await import('date-fns/locale/zh-CN')
|
||||
|
||||
break
|
||||
case 'zh-TW':
|
||||
result = await import('date-fns/locale/zh-TW')
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if (result.default) return result.default
|
||||
|
||||
return result as Locale
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export default {
|
||||
export const ar = {
|
||||
dateFNSKey: 'ar',
|
||||
translations: {
|
||||
authentication: {
|
||||
account: 'الحساب',
|
||||
accountOfCurrentUser: 'حساب المستخدم الحالي',
|
||||
@@ -110,7 +112,8 @@ export default {
|
||||
block: 'وحدة محتوى',
|
||||
blockType: 'نوع وحدة المحتوى',
|
||||
blocks: 'وحدات المحتوى',
|
||||
chooseBetweenCustomTextOrDocument: 'اختر بين إدخال عنوان URL نصّي مخصّص أو الرّبط بمستند آخر.',
|
||||
chooseBetweenCustomTextOrDocument:
|
||||
'اختر بين إدخال عنوان URL نصّي مخصّص أو الرّبط بمستند آخر.',
|
||||
chooseDocumentToLink: 'اختر مستندًا للربط',
|
||||
chooseFromExisting: 'اختر من القائمة',
|
||||
chooseLabel: 'اختر {{label}}',
|
||||
@@ -320,11 +323,13 @@ export default {
|
||||
requiresAtLeast: 'هذا الحقل يتطلب على الأقل {{count}} {{label}}.',
|
||||
requiresNoMoreThan: 'هذا الحقل يتطلب عدم تجاوز {{count}} {{label}}.',
|
||||
requiresTwoNumbers: 'هذا الحقل يتطلب رقمين.',
|
||||
shorterThanMax: 'يجب أن تكون هذه القيمة أقصر من الحد الأقصى للطول الذي هو {{maxLength}} أحرف.',
|
||||
shorterThanMax:
|
||||
'يجب أن تكون هذه القيمة أقصر من الحد الأقصى للطول الذي هو {{maxLength}} أحرف.',
|
||||
trueOrFalse: 'يمكن أن يكون هذا الحقل مساويًا فقط للقيمتين صحيح أو خطأ.',
|
||||
validUploadID: 'هذا الحقل ليس معرّف تحميل صالح.',
|
||||
},
|
||||
version: {
|
||||
type: 'النّوع',
|
||||
aboutToPublishSelection: 'أنت على وشك نشر كلّ {{label}} في التّحديد. هل أنت متأكّد؟',
|
||||
aboutToRestore:
|
||||
'أنت على وشك استرجاع هذا المستند {{label}} إلى الحالة التّي كان عليها في {{versionDate}}.',
|
||||
@@ -365,7 +370,6 @@ export default {
|
||||
showLocales: 'اظهر اللّغات:',
|
||||
showingVersionsFor: 'يتمّ عرض النًّسخ ل:',
|
||||
status: 'الحالة',
|
||||
type: 'النّوع',
|
||||
unpublish: 'الغاء النّشر',
|
||||
unpublishing: 'يتمّ الغاء النّشر...',
|
||||
version: 'النّسخة',
|
||||
@@ -381,4 +385,5 @@ export default {
|
||||
viewingVersions: 'يتمّ استعراض النُّسَخ ل {{entityLabel}} {{documentTitle}}',
|
||||
viewingVersionsGlobal: 'يتمّ استعراض النُّسَخ للاعداد العامّ {{entityLabel}}',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export default {
|
||||
export const de = {
|
||||
dateFNSKey: 'de',
|
||||
translations: {
|
||||
authentication: {
|
||||
account: 'Konto',
|
||||
accountOfCurrentUser: 'Aktuelles Benutzerkonto',
|
||||
@@ -322,7 +324,8 @@ export default {
|
||||
invalidSelections: "'Dieses Feld enthält die folgenden inkorrekten Auswahlen:'",
|
||||
lessThanMin: '{{value}} ist kleiner als der minimal erlaubte {{label}} von {{min}}.',
|
||||
limitReached: 'Limit erreicht, es können nur {{max}} Elemente hinzugefügt werden.',
|
||||
longerThanMin: 'Dieser Wert muss länger als die minimale Länge von {{minLength}} Zeichen sein.',
|
||||
longerThanMin:
|
||||
'Dieser Wert muss länger als die minimale Länge von {{minLength}} Zeichen sein.',
|
||||
notValidDate: '"{{value}}" ist kein gültiges Datum.',
|
||||
required: 'Pflichtfeld',
|
||||
requiresAtLeast: 'Dieses Feld muss mindestens {{count}} {{label}} enthalten.',
|
||||
@@ -333,6 +336,7 @@ export default {
|
||||
validUploadID: "'Dieses Feld enthält keine valide Upload-ID.'",
|
||||
},
|
||||
version: {
|
||||
type: 'Typ',
|
||||
aboutToPublishSelection:
|
||||
'Sie sind dabei, alle {{label}} in der Auswahl zu veröffentlichen. Bist du dir sicher?',
|
||||
aboutToRestore: 'Du bist dabei, {{label}} auf den Stand vom {{versionDate}} zurücksetzen.',
|
||||
@@ -375,7 +379,6 @@ export default {
|
||||
showLocales: 'Sprachumgebungen anzeigen:',
|
||||
showingVersionsFor: 'Versionen anzeigen für:',
|
||||
status: 'Status',
|
||||
type: 'Typ',
|
||||
unpublish: 'Auf Entwurf setzen',
|
||||
unpublishing: 'Setze auf Entwurf...',
|
||||
version: 'Version',
|
||||
@@ -391,4 +394,5 @@ export default {
|
||||
viewingVersions: 'Betrachte Versionen für {{entityLabel}} {{documentTitle}}',
|
||||
viewingVersionsGlobal: '`Betrachte Versionen für das Globale Dokument {{entityLabel}}',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export default {
|
||||
export const en = {
|
||||
dateFNSKey: 'en-US',
|
||||
translations: {
|
||||
authentication: {
|
||||
account: 'Account',
|
||||
accountOfCurrentUser: 'Account of current user',
|
||||
@@ -175,10 +177,10 @@ export default {
|
||||
create: 'Create',
|
||||
createNew: 'Create New',
|
||||
createNewLabel: 'Create new {{label}}',
|
||||
creatingNewLabel: 'Creating new {{label}}',
|
||||
created: 'Created',
|
||||
createdAt: 'Created At',
|
||||
creating: 'Creating',
|
||||
creatingNewLabel: 'Creating new {{label}}',
|
||||
dark: 'Dark',
|
||||
dashboard: 'Dashboard',
|
||||
delete: 'Delete',
|
||||
@@ -228,8 +230,8 @@ export default {
|
||||
notFound: 'Not Found',
|
||||
nothingFound: 'Nothing found',
|
||||
of: 'of',
|
||||
or: 'Or',
|
||||
open: 'Open',
|
||||
or: 'Or',
|
||||
order: 'Order',
|
||||
pageNotFound: 'Page not found',
|
||||
password: 'Password',
|
||||
@@ -291,11 +293,11 @@ export default {
|
||||
dragAndDrop: 'Drag and drop a file',
|
||||
dragAndDropHere: 'or drag and drop a file here',
|
||||
editImage: 'Edit Image',
|
||||
fileName: 'File Name',
|
||||
fileSize: 'File Size',
|
||||
focalPoint: 'Focal Point',
|
||||
focalPointDescription:
|
||||
'Drag the focal point directly on the preview or adjust the values below.',
|
||||
fileName: 'File Name',
|
||||
fileSize: 'File Size',
|
||||
height: 'Height',
|
||||
lessInfo: 'Less info',
|
||||
moreInfo: 'More info',
|
||||
@@ -318,7 +320,8 @@ export default {
|
||||
invalidSelections: 'This field has the following invalid selections:',
|
||||
lessThanMin: '{{value}} is less than the min allowed {{label}} of {{min}}.',
|
||||
limitReached: 'Limit reached, only {{max}} items can be added.',
|
||||
longerThanMin: 'This value must be longer than the minimum length of {{minLength}} characters.',
|
||||
longerThanMin:
|
||||
'This value must be longer than the minimum length of {{minLength}} characters.',
|
||||
notValidDate: '"{{value}}" is not a valid date.',
|
||||
required: 'This field is required.',
|
||||
requiresAtLeast: 'This field requires at least {{count}} {{label}}.',
|
||||
@@ -329,6 +332,7 @@ export default {
|
||||
validUploadID: 'This field is not a valid upload ID.',
|
||||
},
|
||||
version: {
|
||||
type: 'Type',
|
||||
aboutToPublishSelection:
|
||||
'You are about to publish all {{label}} in the selection. Are you sure?',
|
||||
aboutToRestore:
|
||||
@@ -372,7 +376,6 @@ export default {
|
||||
showLocales: 'Show locales:',
|
||||
showingVersionsFor: 'Showing versions for:',
|
||||
status: 'Status',
|
||||
type: 'Type',
|
||||
unpublish: 'Unpublish',
|
||||
unpublishing: 'Unpublishing...',
|
||||
version: 'Version',
|
||||
@@ -388,4 +391,5 @@ export default {
|
||||
viewingVersions: 'Viewing versions for the {{entityLabel}} {{documentTitle}}',
|
||||
viewingVersionsGlobal: 'Viewing versions for the global {{entityLabel}}',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,48 +1,56 @@
|
||||
export type LanguageTranslations = {
|
||||
import type { Locale } from 'date-fns'
|
||||
|
||||
import type { acceptedLanguages } from './utilities/init.js'
|
||||
|
||||
export type Language = {
|
||||
dateFNSKey: string
|
||||
translations: {
|
||||
[namespace: string]: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Translations = {
|
||||
[language: string]: LanguageTranslations
|
||||
export type AcceptedLanguages = (typeof acceptedLanguages)[number]
|
||||
|
||||
export type SupportedLanguages = {
|
||||
[key in AcceptedLanguages]?: Language
|
||||
}
|
||||
|
||||
export type TFunction = (key: string, options?: Record<string, any>) => string
|
||||
|
||||
export type I18n = {
|
||||
dateFNS: Locale
|
||||
/** Corresponding dateFNS key */
|
||||
dateFNSKey: string
|
||||
/** The fallback language */
|
||||
fallbackLanguage: string
|
||||
/** The language of the request */
|
||||
language: string
|
||||
/** Translate function */
|
||||
t: TFunction
|
||||
translations: Translations
|
||||
translations: Language['translations']
|
||||
}
|
||||
|
||||
export type I18nOptions = {
|
||||
fallbackLanguage?: string
|
||||
supportedLanguages?: Translations
|
||||
translations?: {
|
||||
[language: string]:
|
||||
| {
|
||||
$schema: string
|
||||
}
|
||||
| LanguageTranslations
|
||||
}
|
||||
supportedLanguages?: SupportedLanguages
|
||||
translations?: Partial<{
|
||||
[key in AcceptedLanguages]?: Language['translations']
|
||||
}>
|
||||
}
|
||||
|
||||
export type InitTFunction = (args: {
|
||||
config: I18nOptions
|
||||
language?: string
|
||||
translations: Translations
|
||||
translations: Language['translations']
|
||||
}) => {
|
||||
t: TFunction
|
||||
translations: Translations
|
||||
translations: Language['translations']
|
||||
}
|
||||
|
||||
export type InitI18n = (args: {
|
||||
config: I18nOptions
|
||||
context: 'api' | 'client'
|
||||
language?: string
|
||||
}) => I18n
|
||||
}) => Promise<I18n>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Translations } from '../types.js'
|
||||
import type { Language } from '../types.js'
|
||||
|
||||
import { clientTranslationKeys } from '../clientKeys.js'
|
||||
|
||||
@@ -54,18 +54,10 @@ function sortObject(obj) {
|
||||
return sortedObject
|
||||
}
|
||||
|
||||
export const reduceLanguages = (supportedLanguages: Translations, context: 'api' | 'client') => {
|
||||
const languages = {}
|
||||
|
||||
Object.entries(supportedLanguages).forEach(([lang, translations]) => {
|
||||
export const getTranslationsByContext = (translations: Language, context: 'api' | 'client') => {
|
||||
if (context === 'client') {
|
||||
const clientTranslations = sortObject(filterKeys(translations, '', clientTranslationKeys))
|
||||
|
||||
languages[lang] = clientTranslations
|
||||
return sortObject(filterKeys(translations, '', clientTranslationKeys))
|
||||
} else {
|
||||
languages[lang] = translations
|
||||
return translations
|
||||
}
|
||||
})
|
||||
|
||||
return languages
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { I18n, InitI18n, InitTFunction, Translations } from '../types.js'
|
||||
import type { I18n, InitI18n, InitTFunction, Language } from '../types.js'
|
||||
|
||||
import { importDateFNSLocale } from '../importDateFNSLocale.js'
|
||||
import { deepMerge } from './deepMerge.js'
|
||||
import { reduceLanguages } from './reduceLanguages.js'
|
||||
import { getTranslationsByContext } from './getTranslationsByContext.js'
|
||||
|
||||
/**
|
||||
* @function getTranslationString
|
||||
@@ -17,7 +18,7 @@ export const getTranslationString = ({
|
||||
}: {
|
||||
count?: number
|
||||
key: string
|
||||
translations: Translations[0]
|
||||
translations: Language['translations']
|
||||
}) => {
|
||||
const keys = key.split(':')
|
||||
let keySuffix = ''
|
||||
@@ -104,7 +105,7 @@ type TFunctionConstructor = ({
|
||||
vars,
|
||||
}: {
|
||||
key: string
|
||||
translations?: Translations[0]
|
||||
translations?: Language['translations']
|
||||
vars?: Record<string, any>
|
||||
}) => string
|
||||
|
||||
@@ -178,7 +179,7 @@ export const acceptedLanguages = [
|
||||
'vi',
|
||||
'zh',
|
||||
'zhTw',
|
||||
]
|
||||
] as const
|
||||
|
||||
export function matchLanguage(header: string): string | undefined {
|
||||
const parsedHeader = parseAcceptLanguage(header)
|
||||
@@ -196,14 +197,13 @@ export function matchLanguage(header: string): string | undefined {
|
||||
|
||||
const initTFunction: InitTFunction = (args) => {
|
||||
const { config, language, translations } = args
|
||||
const mergedTranslations = deepMerge(config?.translations ?? {}, translations)
|
||||
const languagePreference = matchLanguage(language)
|
||||
const mergedTranslations = deepMerge(config?.translations?.[language] ?? {}, translations)
|
||||
|
||||
return {
|
||||
t: (key, vars) => {
|
||||
return t({
|
||||
key,
|
||||
translations: mergedTranslations[languagePreference],
|
||||
translations: mergedTranslations,
|
||||
vars,
|
||||
})
|
||||
},
|
||||
@@ -211,14 +211,14 @@ const initTFunction: InitTFunction = (args) => {
|
||||
}
|
||||
}
|
||||
|
||||
function memoize(fn: (args: unknown) => I18n, keys: string[]) {
|
||||
function memoize(fn: (args: unknown) => Promise<I18n>, keys: string[]) {
|
||||
const cacheMap = new Map()
|
||||
|
||||
const memoized = (args) => {
|
||||
const memoized = async (args) => {
|
||||
const cacheKey = keys.reduce((acc, key) => acc + args[key], '')
|
||||
|
||||
if (!cacheMap.has(cacheKey)) {
|
||||
const result = fn(args)
|
||||
const result = await fn(args)
|
||||
cacheMap.set(cacheKey, result)
|
||||
}
|
||||
|
||||
@@ -229,20 +229,29 @@ function memoize(fn: (args: unknown) => I18n, keys: string[]) {
|
||||
}
|
||||
|
||||
export const initI18n: InitI18n = memoize(
|
||||
({ config, context, language = 'en' }: Parameters<InitI18n>[0]) => {
|
||||
const languages = reduceLanguages(config.supportedLanguages, context)
|
||||
async ({ config, context, language = 'en' }: Parameters<InitI18n>[0]) => {
|
||||
const translations = getTranslationsByContext(
|
||||
config.supportedLanguages[language].translations,
|
||||
context,
|
||||
)
|
||||
|
||||
const { t, translations } = initTFunction({
|
||||
const { t, translations: mergedTranslations } = initTFunction({
|
||||
config,
|
||||
language: language || config.fallbackLanguage,
|
||||
translations: languages,
|
||||
translations,
|
||||
})
|
||||
|
||||
const dateFNSKey = config.supportedLanguages[language]?.dateFNSKey || 'en-US'
|
||||
|
||||
const dateFNS = await importDateFNSLocale(dateFNSKey)
|
||||
|
||||
const i18n: I18n = {
|
||||
dateFNS,
|
||||
dateFNSKey,
|
||||
fallbackLanguage: config.fallbackLanguage,
|
||||
language: language || config.fallbackLanguage,
|
||||
t,
|
||||
translations,
|
||||
translations: mergedTranslations,
|
||||
}
|
||||
|
||||
return i18n
|
||||
|
||||
@@ -131,7 +131,7 @@ export const Autosave: React.FC<Props> = ({
|
||||
{!saving && lastSaved && (
|
||||
<React.Fragment>
|
||||
{t('version:lastSavedAgo', {
|
||||
distance: formatTimeToNow(lastSaved, i18n.language),
|
||||
distance: formatTimeToNow({ date: lastSaved, i18n }),
|
||||
})}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { Props } from './types.js'
|
||||
import { Calendar as CalendarIcon } from '../../icons/Calendar/index.js'
|
||||
import { X as XIcon } from '../../icons/X/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { getDateLocale } from '../../utilities/getDateLocale.js'
|
||||
import { getFormattedLocale } from './getFormattedLocale.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'date-time-picker'
|
||||
@@ -37,10 +37,10 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
// Use the user's AdminUI language preference for the locale
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const { locale, localeData } = getDateLocale(i18n.language)
|
||||
const datepickerLocale = getFormattedLocale(i18n.language)
|
||||
|
||||
try {
|
||||
registerLocale(locale, localeData)
|
||||
registerLocale(datepickerLocale, i18n.dateFNS)
|
||||
} catch (e) {
|
||||
console.warn(`Could not find DatePicker locale for ${i18n.language}`)
|
||||
}
|
||||
@@ -108,7 +108,7 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
<ReactDatePicker
|
||||
{...dateTimePickerProps}
|
||||
dropdownMode="select"
|
||||
locale={locale}
|
||||
locale={datepickerLocale}
|
||||
showMonthDropdown
|
||||
showYearDropdown
|
||||
/>
|
||||
|
||||
12
packages/ui/src/elements/DatePicker/getFormattedLocale.ts
Normal file
12
packages/ui/src/elements/DatePicker/getFormattedLocale.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const getFormattedLocale = (language = 'enUS') => {
|
||||
const formattedLocales = {
|
||||
en: 'enUS',
|
||||
my: 'enUS', // Burmese is not currently supported
|
||||
ua: 'uk',
|
||||
zh: 'zhCN',
|
||||
}
|
||||
|
||||
const formattedLocale = formattedLocales[language] || language
|
||||
|
||||
return formattedLocale
|
||||
}
|
||||
@@ -122,13 +122,15 @@ export const DocumentControls: React.FC<{
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
title={
|
||||
data?.updatedAt ? formatDate(data?.updatedAt, dateFormat, i18n.language) : ''
|
||||
data?.updatedAt
|
||||
? formatDate({ date: data?.updatedAt, i18n, pattern: dateFormat })
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<p className={`${baseClass}__label`}>{i18n.t('general:lastModified')}: </p>
|
||||
{data?.updatedAt && (
|
||||
<p className={`${baseClass}__value`}>
|
||||
{formatDate(data.updatedAt, dateFormat, i18n.language)}
|
||||
{formatDate({ date: data?.updatedAt, i18n, pattern: dateFormat })}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
@@ -137,13 +139,15 @@ export const DocumentControls: React.FC<{
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
title={
|
||||
data?.createdAt ? formatDate(data?.createdAt, dateFormat, i18n.language) : ''
|
||||
data?.createdAt
|
||||
? formatDate({ date: data?.createdAt, i18n, pattern: dateFormat })
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<p className={`${baseClass}__label`}>{i18n.t('general:created')}: </p>
|
||||
{data?.createdAt && (
|
||||
<p className={`${baseClass}__value`}>
|
||||
{formatDate(data?.createdAt, dateFormat, i18n.language)}
|
||||
{formatDate({ date: data?.createdAt, i18n, pattern: dateFormat })}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
|
||||
@@ -18,5 +18,5 @@ export const DateCell: React.FC<DefaultCellComponentProps<Date | number | string
|
||||
|
||||
const dateFormat = dateDisplayFormat || dateFormatFromConfig
|
||||
|
||||
return <span>{cellData && formatDate(cellData, dateFormat, i18n.language)}</span>
|
||||
return <span>{cellData && formatDate({ date: cellData, i18n, pattern: dateFormat })}</span>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { LanguageTranslations } from '@payloadcms/translations'
|
||||
import type { Language } from '@payloadcms/translations'
|
||||
import type { ClientConfig } from 'payload/types'
|
||||
|
||||
import * as facelessUIImport from '@faceless-ui/modal'
|
||||
@@ -33,19 +33,21 @@ type Props = {
|
||||
children: React.ReactNode
|
||||
componentMap: ComponentMap
|
||||
config: ClientConfig
|
||||
dateFNSKey: Language['dateFNSKey']
|
||||
fallbackLang: ClientConfig['i18n']['fallbackLanguage']
|
||||
lang: string
|
||||
languageCode: string
|
||||
languageOptions: LanguageOptions
|
||||
switchLanguageServerAction?: (lang: string) => Promise<void>
|
||||
translations: LanguageTranslations
|
||||
translations: Language['translations']
|
||||
}
|
||||
|
||||
export const RootProvider: React.FC<Props> = ({
|
||||
children,
|
||||
componentMap,
|
||||
config,
|
||||
dateFNSKey,
|
||||
fallbackLang,
|
||||
lang,
|
||||
languageCode,
|
||||
languageOptions,
|
||||
switchLanguageServerAction,
|
||||
translations,
|
||||
@@ -65,8 +67,9 @@ export const RootProvider: React.FC<Props> = ({
|
||||
<FieldComponentsProvider>
|
||||
<ClientFunctionProvider>
|
||||
<TranslationProvider
|
||||
dateFNSKey={dateFNSKey}
|
||||
fallbackLang={fallbackLang}
|
||||
lang={lang}
|
||||
language={languageCode}
|
||||
languageOptions={languageOptions}
|
||||
switchLanguageServerAction={switchLanguageServerAction}
|
||||
translations={translations}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
'use client'
|
||||
import type { I18n, LanguageTranslations } from '@payloadcms/translations'
|
||||
import type { I18n, Language } from '@payloadcms/translations'
|
||||
import type { Locale } from 'date-fns'
|
||||
import type { ClientConfig } from 'payload/types'
|
||||
|
||||
import { t } from '@payloadcms/translations'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
import { importDateFNSLocale } from '@payloadcms/translations'
|
||||
import enUS from 'date-fns/locale/en-US'
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import { useRouteCache } from '../RouteCache/index.js'
|
||||
|
||||
@@ -19,6 +22,8 @@ const Context = createContext<{
|
||||
t: (key: string, vars?: Record<string, any>) => string
|
||||
}>({
|
||||
i18n: {
|
||||
dateFNS: enUS,
|
||||
dateFNSKey: 'en-US',
|
||||
fallbackLanguage: 'en',
|
||||
language: 'en',
|
||||
t: (key) => key,
|
||||
@@ -31,22 +36,25 @@ const Context = createContext<{
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
dateFNSKey: Language['dateFNSKey']
|
||||
fallbackLang: ClientConfig['i18n']['fallbackLanguage']
|
||||
lang: string
|
||||
language: string
|
||||
languageOptions: LanguageOptions
|
||||
switchLanguageServerAction: (lang: string) => Promise<void>
|
||||
translations: LanguageTranslations
|
||||
translations: Language['translations']
|
||||
}
|
||||
|
||||
export const TranslationProvider: React.FC<Props> = ({
|
||||
children,
|
||||
dateFNSKey,
|
||||
fallbackLang,
|
||||
lang,
|
||||
language,
|
||||
languageOptions,
|
||||
switchLanguageServerAction,
|
||||
translations,
|
||||
}) => {
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
const [dateFNS, setDateFNS] = useState<Locale>()
|
||||
|
||||
const nextT = (key: string, vars?: Record<string, unknown>): string =>
|
||||
t({
|
||||
@@ -68,16 +76,26 @@ export const TranslationProvider: React.FC<Props> = ({
|
||||
[switchLanguageServerAction, clearRouteCache],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const loadDateFNS = async () => {
|
||||
const imported = await importDateFNSLocale(dateFNSKey)
|
||||
|
||||
setDateFNS(imported)
|
||||
}
|
||||
|
||||
void loadDateFNS()
|
||||
}, [dateFNSKey])
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={{
|
||||
i18n: {
|
||||
dateFNS,
|
||||
dateFNSKey,
|
||||
fallbackLanguage: fallbackLang,
|
||||
language: lang,
|
||||
language,
|
||||
t: nextT,
|
||||
translations: {
|
||||
[lang]: translations,
|
||||
},
|
||||
translations,
|
||||
},
|
||||
languageOptions,
|
||||
switchLanguage,
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
|
||||
import { format, formatDistanceToNow } from 'date-fns'
|
||||
|
||||
import { getDateLocale } from './getDateLocale.js'
|
||||
|
||||
export const formatDate = (
|
||||
date: Date | number | string | undefined,
|
||||
pattern: string,
|
||||
locale?: string,
|
||||
): string => {
|
||||
const theDate = new Date(date)
|
||||
const { localeData } = getDateLocale(locale)
|
||||
return format(theDate, pattern, { locale: localeData })
|
||||
type FormatDateArgs = {
|
||||
date: Date | number | string | undefined
|
||||
i18n: I18n
|
||||
pattern: string
|
||||
}
|
||||
|
||||
export const formatTimeToNow = (
|
||||
date: Date | number | string | undefined,
|
||||
locale?: string,
|
||||
): string => {
|
||||
export const formatDate = ({ date, i18n, pattern }: FormatDateArgs): string => {
|
||||
const theDate = new Date(date)
|
||||
const { localeData } = getDateLocale(locale)
|
||||
return formatDistanceToNow(theDate, { locale: localeData })
|
||||
return format(theDate, pattern, { locale: i18n.dateFNS })
|
||||
}
|
||||
|
||||
type FormatTimeToNowArgs = {
|
||||
date: Date | number | string | undefined
|
||||
i18n: I18n
|
||||
}
|
||||
|
||||
export const formatTimeToNow = ({ date, i18n }: FormatTimeToNowArgs): string => {
|
||||
const theDate = new Date(date)
|
||||
return formatDistanceToNow(theDate, { locale: i18n.dateFNS })
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const formatDocTitle = ({
|
||||
const dateFormat =
|
||||
('date' in fieldConfig.admin && fieldConfig?.admin?.date?.displayFormat) ||
|
||||
dateFormatFromConfig
|
||||
title = formatDate(title, dateFormat, i18n.language) || title
|
||||
title = formatDate({ date: title, i18n, pattern: dateFormat }) || title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { Locale } from 'date-fns'
|
||||
|
||||
import { dateLocales } from './dateLocales.js'
|
||||
|
||||
export const getDateLocale = (
|
||||
locale = 'enUS',
|
||||
): {
|
||||
locale: string
|
||||
localeData: Locale
|
||||
} => {
|
||||
const formattedLocales = {
|
||||
en: 'enUS',
|
||||
my: 'enUS', // Burmese is not currently supported
|
||||
ua: 'uk',
|
||||
zh: 'zhCN',
|
||||
}
|
||||
|
||||
const formattedLocale = formattedLocales[locale] || locale
|
||||
|
||||
return {
|
||||
locale: formattedLocale,
|
||||
localeData: dateLocales[formattedLocale],
|
||||
}
|
||||
}
|
||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@@ -1345,6 +1345,9 @@ importers:
|
||||
'@types/react':
|
||||
specifier: 18.2.74
|
||||
version: 18.2.74
|
||||
date-fns:
|
||||
specifier: 3.3.1
|
||||
version: 3.3.1
|
||||
typescript:
|
||||
specifier: 5.4.4
|
||||
version: 5.4.4
|
||||
@@ -8358,7 +8361,6 @@ packages:
|
||||
|
||||
/date-fns@3.3.1:
|
||||
resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==}
|
||||
dev: false
|
||||
|
||||
/dateformat@4.6.3:
|
||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
UploadFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import { de } from '@payloadcms/translations/languages/de'
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
// import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
import { type Config, buildConfig } from 'payload/config'
|
||||
import sharp from 'sharp'
|
||||
@@ -171,6 +173,12 @@ export async function buildConfigWithDefaults(
|
||||
}),
|
||||
sharp,
|
||||
telemetry: false,
|
||||
i18n: {
|
||||
supportedLanguages: {
|
||||
en,
|
||||
de,
|
||||
},
|
||||
},
|
||||
typescript: {
|
||||
declare: false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user