Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/configuration/localization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,29 @@ localization: {

Since the filtering happens at the root level of the application and its result is not calculated every time you navigate to a new page, you may want to call `router.refresh` in a custom component that watches when values that affect the result change. In the example above, you would want to do this when `supportedLocales` changes on the tenant document.

## Experimental Options

Experimental options are features that may not be fully stable and may change or be removed in future releases.

These options can be enabled in your Payload Config under the `experimental` key. You can set them like this:

```ts
import { buildConfig } from 'payload'

export default buildConfig({
// ...
experimental: {
localizeMeta: true,
},
})
```

The following experimental options are available related to localization:

| Option | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`localizeMeta`** | **Boolean.** When `true`, shows document metadata (e.g., status, updatedAt) per locale in the admin panel instead of showing the latest overall metadata. Defaults to `false`. |

## Field Localization

Payload Localization works on a **field** level—not a document level. In addition to configuring the base Payload Config to support Localization, you need to specify each field that you would like to localize.
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ The following options are available:
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
| **`experimental`** | Configure experimental features for Payload. These may be unstable and may change or be removed in future releases. [More details](../experimental/overview). |
| **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). |
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
Expand Down
45 changes: 45 additions & 0 deletions docs/experimental/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: Experimental Features
label: Overview
order: 10
desc: Enable and configure experimental functionality within Payload. These featuresmay be unstable and may change or be removed without notice.
keywords: experimental, unstable, beta, preview, features, configuration, Payload, cms, headless, javascript, node, react, nextjs
---

Experimental features allow you to try out new functionality before it becomes a stable part of Payload. These features may still be in active development, may have incomplete functionality, and can change or be removed in future releases without warning.

## How It Works

Experimental features are configured via the root-level `experimental` property in your [Payload Config](../configuration/overview). This property contains individual feature flags, each flag can be configured independently, allowing you to selectively opt into specific functionality.

```ts
import { buildConfig } from 'payload'

const config = buildConfig({
// ...
experimental: {
localizeMeta: true, // highlight-line
},
})
```

## Experimental Options

The following options are available:

| Option | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`localizeMeta`** | **Boolean.** When `true`, shows document metadata (e.g., status, updatedAt) per locale in the admin panel instead of showing the latest overall metadata. Defaults to `false`. |

This list may change without notice.

## When to Use Experimental Features

You might enable an experimental feature when:

- You want early access to new capabilities before their stable release.
- You can accept the risks of using potentially unstable functionality.
- You are testing new features in a development or staging environment.
- You wish to provide feedback to the Payload team on new functionality.

If you are working on a production application, carefully evaluate whether the benefits outweigh the risks. For most stable applications, it is recommended to wait until the feature is officially released.
5 changes: 5 additions & 0 deletions packages/payload/src/collections/config/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
import { authCollectionEndpoints } from '../../auth/endpoints/index.js'
import { getBaseAuthFields } from '../../auth/getAuthFields.js'
import { TimestampsRequired } from '../../errors/TimestampsRequired.js'
import { baseLocalizedMetaFields } from '../../fields/baseFields/baseLocalizedMeta.js'
import { sanitizeFields } from '../../fields/config/sanitize.js'
import { fieldAffectsData } from '../../fields/config/types.js'
import { mergeBaseFields } from '../../fields/mergeBaseFields.js'
Expand Down Expand Up @@ -261,6 +262,10 @@ export const sanitizeCollection = async (
sanitized.fields = mergeBaseFields(sanitized.fields, getBaseAuthFields(sanitized.auth))
}

if (config.localization && collection.versions && config.experimental?.localizeMeta) {
sanitized.fields = mergeBaseFields(sanitized.fields, baseLocalizedMetaFields(config))
}

if (collection?.admin?.pagination?.limits?.length) {
sanitized.admin!.pagination!.limits = collection.admin.pagination.limits
}
Expand Down
19 changes: 19 additions & 0 deletions packages/payload/src/collections/operations/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { uploadFiles } from '../../uploads/uploadFiles.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { populateLocalizedMeta } from '../../utilities/populateLocalizedMeta.js'
import { sanitizeInternalFields } from '../../utilities/sanitizeInternalFields.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { buildAfterOperation } from './utils.js'
Expand Down Expand Up @@ -140,6 +141,24 @@ export const createOperation = async <
duplicatedFromDocWithLocales = duplicateResult.duplicatedFromDocWithLocales
}

if (
config.experimental?.localizeMeta &&
config.localization &&
config.localization.locales.length > 0 &&
locale
) {
duplicatedFromDocWithLocales._localizedMeta = populateLocalizedMeta({
config,
locale,
previousMeta: duplicatedFromDocWithLocales._localizedMeta,
publishSpecificLocale,
status: data._status,
})

data._localizedMeta = duplicatedFromDocWithLocales._localizedMeta[locale]
duplicatedFromDoc._localizedMeta = duplicatedFromDocWithLocales._localizedMeta[locale]
}

// /////////////////////////////////////
// Access
// /////////////////////////////////////
Expand Down
22 changes: 22 additions & 0 deletions packages/payload/src/collections/operations/utilities/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { uploadFiles } from '../../../uploads/uploadFiles.js'
import { checkDocumentLockStatus } from '../../../utilities/checkDocumentLockStatus.js'
import { filterDataToSelectedLocales } from '../../../utilities/filterDataToSelectedLocales.js'
import { mergeLocalizedData } from '../../../utilities/mergeLocalizedData.js'
import { populateLocalizedMeta } from '../../../utilities/populateLocalizedMeta.js'
import { getLatestCollectionVersion } from '../../../versions/getLatestCollectionVersion.js'

export type SharedUpdateDocumentArgs<TSlug extends CollectionSlug> = {
Expand Down Expand Up @@ -224,6 +225,27 @@ export const updateDocument = async <
}
}

// /////////////////////////////////////
// Handle localizedMeta
// /////////////////////////////////////

if (
config.experimental?.localizeMeta &&
config.localization &&
config.localization.locales.length > 0
) {
docWithLocales._localizedMeta = populateLocalizedMeta({
config,
locale,
previousMeta: docWithLocales._localizedMeta,
publishSpecificLocale,
status: data._status,
})

data._localizedMeta = docWithLocales._localizedMeta[locale]
originalDoc._localizedMeta = docWithLocales._localizedMeta[locale]
}

// /////////////////////////////////////
// beforeChange - Fields
// /////////////////////////////////////
Expand Down
10 changes: 10 additions & 0 deletions packages/payload/src/config/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@ export const createClientConfig = ({

break

case 'experimental':
if (config.experimental) {
clientConfig.experimental = {}
if (config.experimental?.localizeMeta) {
clientConfig.experimental.localizeMeta = config.experimental.localizeMeta
}
}

break

case 'folders':
if (config.folders) {
clientConfig.folders = {
Expand Down
2 changes: 2 additions & 0 deletions packages/payload/src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
defaultDepth: 2,
defaultMaxTextLength: 40000,
endpoints: [],
experimental: {},
globals: [],
graphQL: {
disablePlaygroundInProduction: true,
Expand Down Expand Up @@ -125,6 +126,7 @@ export const addDefaultsToConfig = (config: Config): Config => {
config.defaultDepth = config.defaultDepth ?? 2
config.defaultMaxTextLength = config.defaultMaxTextLength ?? 40000
config.endpoints = config.endpoints ?? []
config.experimental = config.experimental || {}
config.globals = config.globals ?? []
config.graphQL = {
disableIntrospectionInProduction: true,
Expand Down
17 changes: 17 additions & 0 deletions packages/payload/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,17 @@ export type AfterErrorHook = (
args: AfterErrorHookArgs,
) => AfterErrorResult | Promise<AfterErrorResult>

/**
* Experimental features.
* These may be unstable and may change or be removed in future releases.
*/
export type ExperimentalConfig = {
/**
* When `true`, shows document metadata (e.g., status, updatedAt) per locale in the admin panel instead of showing the latest overall metadata. Defaults to `false`.
*/
localizeMeta?: boolean
}

/**
* This is the central configuration
*
Expand Down Expand Up @@ -1087,6 +1098,12 @@ export type Config = {
email?: EmailAdapter | Promise<EmailAdapter>
/** Custom REST endpoints */
endpoints?: Endpoint[]
/**
* Configure experimental features for Payload.
*
* These features may be unstable and may change or be removed in future releases.
*/
experimental?: ExperimentalConfig
/**
* Options for folder view within the admin panel
*
Expand Down
36 changes: 36 additions & 0 deletions packages/payload/src/fields/baseFields/baseLocalizedMeta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Config, SanitizedConfig } from '../../config/types.js'
import type { Field } from '../config/types.js'
export const baseLocalizedMetaFields = (config: Config | SanitizedConfig): Field[] => {
if (!config.localization || !config.localization.locales) {
return []
}

return [
{
name: '_localizedMeta',
type: 'group',
admin: {
disableBulkEdit: true,
disableListColumn: true,
disableListFilter: true,
hidden: true,
},
fields: [
{
name: 'status',
type: 'select',
options: [
{ label: ({ t }: any) => t('version:draft'), value: 'draft' },
{ label: ({ t }: any) => t('version:published'), value: 'published' },
],
},
{
name: 'updatedAt',
type: 'date',
},
] as Field[],
label: ({ t }: any) => t('localization:localizedMeta'),
localized: true,
},
]
}
59 changes: 59 additions & 0 deletions packages/payload/src/utilities/populateLocalizedMeta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { LocalizedMeta } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'

/**
* Returned object can be directly assigned to `data.localizedMeta` when saving a document
*/
export function populateLocalizedMeta(args: {
config: SanitizedConfig
locale: string
previousMeta: LocalizedMeta
publishSpecificLocale?: string
status: 'draft' | 'published'
}): LocalizedMeta {
const { config, locale, previousMeta, publishSpecificLocale, status } = args

if (!config.localization) {
return {}
}

const now = new Date().toISOString()
const localizedMeta: LocalizedMeta = {}

const defaultDraft = (): LocalizedMeta[string] => ({ status: 'draft', updatedAt: now })
const publishedNow = (): LocalizedMeta[string] => ({ status: 'published', updatedAt: now })

for (const code of config.localization.localeCodes) {
const previous = previousMeta?.[code]

if (status === 'draft') {
if (code === locale) {
// Incoming locale is saved as draft
localizedMeta[code] = defaultDraft()
} else {
// Other locales keep previous state or become draft if none existed
localizedMeta[code] = previous || defaultDraft()
}
continue
}

if (status === 'published') {
if (publishSpecificLocale) {
if (code === publishSpecificLocale) {
// Only publish the specified locale
localizedMeta[code] = publishedNow()
} else {
// Other locales keep previous state or become draft if none existed
localizedMeta[code] = previous || defaultDraft()
}
continue
}

// If publishSpecificLocale is false it is publishAll
localizedMeta[code] = publishedNow()
continue
}
}

return localizedMeta
}
3 changes: 3 additions & 0 deletions test/localization/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export default buildConfigWithDefaults({
baseDir: path.resolve(dirname),
},
},
experimental: {
localizeMeta: true,
},
collections: [
RichTextCollection,
BlocksCollection,
Expand Down
Loading
Loading