fix: fallbackLocale not respecting default settings, locale specific fallbacks and not respecting 'none' or false (#8591)

This PR fixes and improves a few things around localisation and
fallbackLocale:
- For the REST API `fallbackLocale` and `fallback-locale` are treated
the same for consistency with the Local API
- `fallback: false` in config is now respected, by default results will
not fallback to `defaultLocale` unless this config is true, can also be
overridden by providing an explicit `fallbackLocale` in the request
- locale specific fallbacks will now take priority over `defaultLocale`
unless an explicit fallback is provided
- Fixes types on operations to allow `'none'` as a value for
fallbackLocale
- `fallback` is now true by default if unspecified

Closes https://github.com/payloadcms/payload/issues/8443
This commit is contained in:
Paul
2024-11-13 12:13:31 -06:00
committed by GitHub
parent 3b55458c0d
commit f4d526d6e5
43 changed files with 2451 additions and 2197 deletions

View File

@@ -65,7 +65,7 @@ export default buildConfig({
},
],
defaultLocale: 'en', // required
fallback: true,
fallback: true, // defaults to true
},
})
```
@@ -81,7 +81,7 @@ The following options are available:
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **`locales`** | Array of all the languages that you would like to support. [More details](#locales) |
| **`defaultLocale`** | Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale. |
| **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated. |
| **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated unless a fallback is explicitly provided in the request. True by default. |
### Locales

View File

@@ -20,7 +20,9 @@ export const Login = ({ tenantSlug }: Props) => {
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
if (!usernameRef?.current?.value || !passwordRef?.current?.value) {return}
if (!usernameRef?.current?.value || !passwordRef?.current?.value) {
return
}
const actionRes = await fetch(
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/external-users/login`,
{

View File

@@ -6,7 +6,9 @@ import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const ensureUniqueSlug: FieldHook = async ({ data, originalDoc, req, value }) => {
// if value is unchanged, skip validation
if (originalDoc.slug === value) {return value}
if (originalDoc.slug === value) {
return value
}
const incomingTenantID = typeof data?.tenant === 'object' ? data.tenant.id : data?.tenant
const currentTenantID =

View File

@@ -14,7 +14,9 @@ export const Pages: CollectionConfig = {
read: (args) => {
// when viewing pages inside the admin panel
// restrict access to the ones your user has access to
if (isPayloadAdminPanel(args.req)) {return filterByTenantRead(args)}
if (isPayloadAdminPanel(args.req)) {
return filterByTenantRead(args)
}
// when viewing pages from outside the admin panel
// you should be able to see your tenants and public tenants

View File

@@ -7,7 +7,9 @@ export const tenantRead: Access = (args) => {
const req = args.req
// Super admin can read all
if (isSuperAdmin(args)) {return true}
if (isSuperAdmin(args)) {
return true
}
const tenantIDs = getTenantAccessIDs(req.user)

View File

@@ -7,13 +7,19 @@ import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAcces
export const createAccess: Access<User> = (args) => {
const { req } = args
if (!req.user) {return false}
if (!req.user) {
return false
}
if (isSuperAdmin(args)) {return true}
if (isSuperAdmin(args)) {
return true
}
const adminTenantAccessIDs = getTenantAdminTenantAccessIDs(req.user)
if (adminTenantAccessIDs.length > 0) {return true}
if (adminTenantAccessIDs.length > 0) {
return true
}
return false
}

View File

@@ -1,6 +1,8 @@
import type { Access } from 'payload'
export const isAccessingSelf: Access = ({ id, req }) => {
if (!req?.user) {return false}
if (!req?.user) {
return false
}
return req.user.id === id
}

View File

@@ -5,9 +5,13 @@ import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAcces
export const updateAndDeleteAccess: Access = (args) => {
const { req } = args
if (!req.user) {return false}
if (!req.user) {
return false
}
if (isSuperAdmin(args)) {return true}
if (isSuperAdmin(args)) {
return true
}
const adminTenantAccessIDs = getTenantAdminTenantAccessIDs(req.user)

View File

@@ -6,7 +6,9 @@ import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req, value }) => {
// if value is unchanged, skip validation
if (originalDoc.username === value) {return value}
if (originalDoc.username === value) {
return value
}
const incomingTenantID = typeof data?.tenant === 'object' ? data.tenant.id : data?.tenant
const currentTenantID =

View File

@@ -8,7 +8,9 @@ export const autofillTenant: FieldHook = ({ req, value }) => {
// return that tenant ID as the value
if (!value) {
const tenantIDs = getTenantAccessIDs(req.user)
if (tenantIDs.length === 1) {return tenantIDs[0]}
if (tenantIDs.length === 1) {
return tenantIDs[0]
}
}
return value

View File

@@ -10,7 +10,9 @@ export const tenantField: Field = {
access: {
read: () => true,
update: (args) => {
if (isSuperAdmin(args)) {return true}
if (isSuperAdmin(args)) {
return true
}
return tenantFieldUpdate(args)
},
},

View File

@@ -1,7 +1,9 @@
import type { User } from '../payload-types'
export const getTenantAccessIDs = (user: null | User): string[] => {
if (!user) {return []}
if (!user) {
return []
}
return (
user?.tenants?.reduce((acc: string[], { tenant }) => {
if (tenant) {
@@ -13,7 +15,9 @@ export const getTenantAccessIDs = (user: null | User): string[] => {
}
export const getTenantAdminTenantAccessIDs = (user: null | User): string[] => {
if (!user) {return []}
if (!user) {
return []
}
return (
user?.tenants?.reduce((acc: string[], { roles, tenant }) => {

View File

@@ -1,5 +1,6 @@
import type { PayloadRequest, SanitizedConfig } from 'payload'
import { sanitizeFallbackLocale } from 'payload'
/**
* Mutates the Request to contain 'locale' and 'fallbackLocale' based on data or searchParams
*/
@@ -17,14 +18,16 @@ export function addLocalesToRequestFromData(req: PayloadRequest): void {
localeOnReq = data.locale
}
if (
!fallbackLocaleOnReq &&
data?.['fallback-locale'] &&
typeof data?.['fallback-locale'] === 'string'
) {
if (!fallbackLocaleOnReq) {
if (data?.['fallback-locale'] && typeof data?.['fallback-locale'] === 'string') {
fallbackLocaleOnReq = data['fallback-locale']
}
if (data?.['fallbackLocale'] && typeof data?.['fallbackLocale'] === 'string') {
fallbackLocaleOnReq = data['fallbackLocale']
}
}
const { fallbackLocale, locale } = sanitizeLocales({
fallbackLocale: fallbackLocaleOnReq,
locale: localeOnReq,
@@ -54,15 +57,19 @@ export const sanitizeLocales = ({
locale,
localization,
}: SanitizeLocalesArgs): SanitizeLocalesReturn => {
if (['none', 'null'].includes(fallbackLocale)) {
fallbackLocale = 'null'
} else if (localization && !localization.localeCodes.includes(fallbackLocale)) {
fallbackLocale = localization.defaultLocale
// Check if localization has fallback enabled or if a fallback locale is provided
if (localization) {
fallbackLocale = sanitizeFallbackLocale({
fallbackLocale,
locale,
localization,
})
}
if (locale === '*') {
locale = 'all'
} else if (localization && !localization.localeCodes.includes(locale)) {
} else if (localization && !localization.localeCodes.includes(locale) && localization.fallback) {
locale = localization.defaultLocale
}

View File

@@ -1,7 +1,7 @@
import type { CustomPayloadRequestProperties, PayloadRequest, SanitizedConfig } from 'payload'
import { initI18n } from '@payloadcms/translations'
import { executeAuthStrategies, getDataLoader, parseCookies } from 'payload'
import { executeAuthStrategies, getDataLoader, parseCookies, sanitizeFallbackLocale } from 'payload'
import * as qs from 'qs-esm'
import { URL } from 'url'
@@ -26,6 +26,7 @@ export const createPayloadRequest = async ({
const payload = await getPayloadHMR({ config: configPromise })
const { config } = payload
const localization = config.localization
const urlProperties = new URL(request.url)
const { pathname, searchParams } = urlProperties
@@ -45,8 +46,10 @@ export const createPayloadRequest = async ({
language,
})
let locale
let fallbackLocale
const fallbackFromRequest =
searchParams.get('fallback-locale') || searchParams.get('fallbackLocale')
let locale = searchParams.get('locale')
let fallbackLocale = fallbackFromRequest
const overrideHttpMethod = request.headers.get('X-HTTP-Method-Override')
const queryToParse = overrideHttpMethod === 'GET' ? await request.text() : urlProperties.search
@@ -59,21 +62,24 @@ export const createPayloadRequest = async ({
})
: {}
if (config.localization) {
const locales = sanitizeLocales({
fallbackLocale: searchParams.get('fallback-locale'),
locale: searchParams.get('locale'),
localization: payload.config.localization,
if (localization) {
fallbackLocale = sanitizeFallbackLocale({
fallbackLocale,
locale,
localization,
})
const locales = sanitizeLocales({
fallbackLocale,
locale,
localization,
})
locale = locales.locale
fallbackLocale = locales.fallbackLocale
// Override if query params are present, in order to respect HTTP method override
if (query.locale) {
locale = query.locale
}
if (query?.['fallback-locale']) {
fallbackLocale = query['fallback-locale']
locale = query.locale as string
}
}

View File

@@ -1,5 +1,7 @@
import type { Payload } from 'payload'
import { sanitizeFallbackLocale } from 'payload'
type GetRequestLocalesArgs = {
data?: Record<string, any>
localization: Exclude<Payload['config']['localization'], false>
@@ -10,7 +12,7 @@ export function getRequestLocales({ data, localization, searchParams }: GetReque
locale: string
} {
let locale = searchParams.get('locale')
let fallbackLocale = searchParams.get('fallback-locale')
let fallbackLocale = searchParams.get('fallback-locale') || searchParams.get('fallbackLocale')
if (data) {
if (data?.locale) {
@@ -19,13 +21,16 @@ export function getRequestLocales({ data, localization, searchParams }: GetReque
if (data?.['fallback-locale']) {
fallbackLocale = data['fallback-locale']
}
if (data?.['fallbackLocale']) {
fallbackLocale = data['fallbackLocale']
}
}
if (fallbackLocale === 'none') {
fallbackLocale = 'null'
} else if (!localization.localeCodes.includes(fallbackLocale)) {
fallbackLocale = localization.defaultLocale
}
fallbackLocale = sanitizeFallbackLocale({
fallbackLocale,
locale,
localization,
})
if (locale === '*') {
locale = 'all'

View File

@@ -44,7 +44,7 @@ export const initPage = async ({
// we get above. Clone the req? We'll look into that eventually.
const req = await createLocalReq(
{
fallbackLocale: null,
fallbackLocale: false,
req: {
headers,
host: headers.get('host'),

View File

@@ -39,7 +39,6 @@ export const initReq = cache(async function (
const req = await createLocalReq(
{
fallbackLocale: 'null',
req: {
headers,
host: headers.get('host'),

View File

@@ -26,7 +26,7 @@ export const getDocumentData = async ({
collection: collectionSlug,
depth: 0,
draft: true,
fallbackLocale: null,
fallbackLocale: false,
locale: locale?.code,
overrideAccess: false,
user,
@@ -38,7 +38,7 @@ export const getDocumentData = async ({
slug: globalSlug,
depth: 0,
draft: true,
fallbackLocale: null,
fallbackLocale: false,
locale: locale?.code,
overrideAccess: false,
user,

View File

@@ -149,6 +149,7 @@ export const renderDocument = async ({
data: doc,
docPermissions,
docPreferences,
fallbackLocale: false,
globalSlug,
locale: locale?.code,
operation: (collectionSlug && id) || globalSlug ? 'update' : 'create',
@@ -278,7 +279,7 @@ export const renderDocument = async ({
data: initialData || {},
depth: 0,
draft: true,
fallbackLocale: null,
fallbackLocale: false,
locale: locale?.code,
req,
user,

View File

@@ -139,7 +139,7 @@ export const renderListView = async (
collection: collectionSlug,
depth: 0,
draft: true,
fallbackLocale: null,
fallbackLocale: false,
includeLockStatus: true,
limit,
locale,

View File

@@ -37,7 +37,7 @@ export const LivePreviewView: PayloadServerReactComponent<EditViewComponent> = a
collection: collectionConfig.slug,
depth: 0,
draft: true,
fallbackLocale: null,
fallbackLocale: false,
})
}
@@ -46,7 +46,7 @@ export const LivePreviewView: PayloadServerReactComponent<EditViewComponent> = a
slug: globalConfig.slug,
depth: 0,
draft: true,
fallbackLocale: null,
fallbackLocale: false,
})
}
} catch (error) {

View File

@@ -2,6 +2,7 @@ import { type SupportedLanguages } from '@payloadcms/translations'
import type { DocumentPermissions } from '../../auth/types.js'
import type { Field, Validate } from '../../fields/config/types.js'
import type { TypedLocale } from '../../index.js'
import type { DocumentPreferences } from '../../preferences/types.js'
import type { PayloadRequest, Where } from '../../types/index.js'
@@ -62,6 +63,7 @@ export type BuildFormStateArgs = {
data?: Data
docPermissions: DocumentPermissions | undefined
docPreferences: DocumentPreferences
fallbackLocale?: false | TypedLocale
formState?: FormState
id?: number | string
/*

View File

@@ -28,7 +28,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
disableTransaction?: boolean
disableVerificationEmail?: boolean
draft?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
file?: File
filePath?: string
locale?: TypedLocale

View File

@@ -22,7 +22,7 @@ export type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType
context?: RequestContext
depth?: number
disableTransaction?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
locale?: TypedLocale
overrideAccess?: boolean
overrideLock?: boolean

View File

@@ -22,7 +22,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
depth?: number
disableTransaction?: boolean
draft?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
id: number | string
locale?: TypedLocale
overrideAccess?: boolean

View File

@@ -31,7 +31,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
depth?: number
disableErrors?: boolean
draft?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
includeLockStatus?: boolean
joins?: JoinQuery<TSlug>
limit?: number

View File

@@ -34,7 +34,7 @@ export type Options<
depth?: number
disableErrors?: TDisableErrors
draft?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
id: number | string
includeLockStatus?: boolean
joins?: JoinQuery<TSlug>

View File

@@ -16,7 +16,7 @@ export type Options<TSlug extends CollectionSlug> = {
depth?: number
disableErrors?: boolean
draft?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
id: string
locale?: 'all' | TypedLocale
overrideAccess?: boolean

View File

@@ -23,7 +23,7 @@ export type Options<TSlug extends CollectionSlug> = {
context?: RequestContext
depth?: number
draft?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
limit?: number
locale?: 'all' | TypedLocale
overrideAccess?: boolean

View File

@@ -14,7 +14,7 @@ export type Options<TSlug extends CollectionSlug> = {
context?: RequestContext
depth?: number
draft?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
id: string
locale?: TypedLocale
overrideAccess?: boolean

View File

@@ -33,7 +33,7 @@ export type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType
depth?: number
disableTransaction?: boolean
draft?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
file?: File
filePath?: string
locale?: TypedLocale

View File

@@ -141,6 +141,9 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
toString: () => locale.code,
}))
}
// Default fallback to true if not provided
config.localization.fallback = config.localization?.fallback ?? true
}
const i18nConfig: SanitizedConfig['i18n'] = {

View File

@@ -444,7 +444,11 @@ export type BaseLocalizationConfig = {
* @example `"en"`
*/
defaultLocale: string
/** Set to `true` to let missing values in localised fields fall back to the values in `defaultLocale` */
/** Set to `true` to let missing values in localised fields fall back to the values in `defaultLocale`
*
* If false, then no requests will fallback unless a fallbackLocale is specified in the request.
* @default true
*/
fallback?: boolean
}

View File

@@ -16,7 +16,7 @@ export type Options<TSlug extends GlobalSlug, TSelect extends SelectType> = {
context?: RequestContext
depth?: number
draft?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
includeLockStatus?: boolean
locale?: 'all' | TypedLocale
overrideAccess?: boolean

View File

@@ -11,7 +11,7 @@ export type Options<TSlug extends GlobalSlug> = {
context?: RequestContext
depth?: number
disableErrors?: boolean
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
id: string
locale?: 'all' | TypedLocale
overrideAccess?: boolean

View File

@@ -19,7 +19,7 @@ import { findVersionsOperation } from '../findVersions.js'
export type Options<TSlug extends GlobalSlug> = {
context?: RequestContext
depth?: number
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
limit?: number
locale?: 'all' | TypedLocale
overrideAccess?: boolean

View File

@@ -10,7 +10,7 @@ import { restoreVersionOperation } from '../restoreVersion.js'
export type Options<TSlug extends GlobalSlug> = {
context?: RequestContext
depth?: number
fallbackLocale?: TypedLocale
fallbackLocale?: false | TypedLocale
id: string
locale?: TypedLocale
overrideAccess?: boolean

View File

@@ -19,8 +19,8 @@ export type Options<TSlug extends GlobalSlug, TSelect extends SelectType> = {
data: DeepPartial<Omit<DataFromGlobalSlug<TSlug>, 'id'>>
depth?: number
draft?: boolean
fallbackLocale?: TypedLocale
locale?: TypedLocale
fallbackLocale?: false | TypedLocale
locale?: 'all' | TypedLocale
overrideAccess?: boolean
overrideLock?: boolean
populate?: PopulateType

View File

@@ -1202,6 +1202,7 @@ export { isPlainObject } from './utilities/isPlainObject.js'
export { isValidID } from './utilities/isValidID.js'
export { killTransaction } from './utilities/killTransaction.js'
export { mapAsync } from './utilities/mapAsync.js'
export { sanitizeFallbackLocale } from './utilities/sanitizeFallbackLocale.js'
export { traverseFields } from './utilities/traverseFields.js'
export type { TraverseFieldsCallback } from './utilities/traverseFields.js'
export { buildVersionCollectionFields } from './versions/buildCollectionFields.js'
@@ -1211,7 +1212,7 @@ export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'
export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js'
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'
export { saveVersion } from './versions/saveVersion.js'
export { saveVersion } from './versions/saveVersion.js'
export type { TypeWithVersion } from './versions/types.js'
export { deepMergeSimple } from '@payloadcms/translations/utilities'

View File

@@ -1,9 +1,10 @@
import type { User } from '../auth/types.js'
import type { Payload, RequestContext } from '../index.js'
import type { Payload, RequestContext, TypedLocale } from '../index.js'
import type { PayloadRequest } from '../types/index.js'
import { getDataLoader } from '../collections/dataloader.js'
import { getLocalI18n } from '../translations/getLocalI18n.js'
import { sanitizeFallbackLocale } from '../utilities/sanitizeFallbackLocale.js'
function getRequestContext(
req: Partial<PayloadRequest> = { context: null } as PayloadRequest,
@@ -71,7 +72,7 @@ const attachFakeURLProperties = (req: Partial<PayloadRequest>) => {
type CreateLocalReq = (
options: {
context?: RequestContext
fallbackLocale?: string
fallbackLocale?: false | TypedLocale
locale?: string
req?: Partial<PayloadRequest>
user?: User
@@ -83,23 +84,22 @@ export const createLocalReq: CreateLocalReq = async (
{ context, fallbackLocale, locale: localeArg, req = {} as PayloadRequest, user },
payload,
) => {
if (payload.config?.localization) {
const localization = payload.config?.localization
if (localization) {
const locale = localeArg === '*' ? 'all' : localeArg
const defaultLocale = payload.config.localization.defaultLocale
const defaultLocale = localization.defaultLocale
const localeCandidate = locale || req?.locale || req?.query?.locale
req.locale =
localeCandidate && typeof localeCandidate === 'string' ? localeCandidate : defaultLocale
const fallbackLocaleFromConfig = payload.config.localization.locales.find(
({ code }) => req.locale === code,
)?.fallbackLocale
const sanitizedFallback = sanitizeFallbackLocale({
fallbackLocale,
locale: req.locale,
localization,
})
if (typeof fallbackLocale !== 'undefined') {
req.fallbackLocale = fallbackLocale
} else if (typeof req?.fallbackLocale === 'undefined') {
req.fallbackLocale = fallbackLocaleFromConfig || defaultLocale
}
req.fallbackLocale = sanitizedFallback
}
const i18n =

View File

@@ -0,0 +1,53 @@
import type { SanitizedLocalizationConfig } from '../config/types.js'
import type { TypedLocale } from '../index.js'
interface Args {
fallbackLocale: false | TypedLocale
locale: string
localization: SanitizedLocalizationConfig
}
/**
* Sanitizes fallbackLocale based on a provided fallbackLocale, locale and localization config
*
* Handles the following scenarios:
* - determines if a fallback locale should be used
* - determines if a locale specific fallback should be used in place of the default locale
* - sets the fallbackLocale to 'null' if no fallback locale should be used
*/
export const sanitizeFallbackLocale = ({
fallbackLocale,
locale,
localization,
}: Args): null | string => {
const hasFallbackLocale =
fallbackLocale === undefined || fallbackLocale === null
? localization && localization.fallback
: fallbackLocale
? !['false', 'none', 'null'].includes(fallbackLocale)
: false
if (hasFallbackLocale) {
if (!fallbackLocale) {
// Check for locale specific fallback
const localeSpecificFallback =
localization && localization?.locales?.length
? localization.locales.find((localeConfig) => localeConfig.code === locale)
?.fallbackLocale
: undefined
if (localeSpecificFallback) {
fallbackLocale = localeSpecificFallback
} else {
// Use defaultLocale as fallback otherwise
if (localization && 'fallback' in localization && localization.fallback) {
fallbackLocale = localization.defaultLocale
}
}
}
} else {
fallbackLocale = null
}
return fallbackLocale as null | string
}

View File

@@ -84,7 +84,7 @@ export const ServerFunctionsProvider: React.FC<{
if (!remoteSignal?.aborted) {
const result = (await serverFunction({
name: 'form-state',
args: rest,
args: { fallbackLocale: false, ...rest },
})) as ReturnType<typeof buildFormStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
if (!remoteSignal?.aborted) {
@@ -108,7 +108,7 @@ export const ServerFunctionsProvider: React.FC<{
if (!remoteSignal?.aborted) {
const result = (await serverFunction({
name: 'table-state',
args: rest,
args: { fallbackLocale: false, ...rest },
})) as ReturnType<typeof buildTableStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
if (!remoteSignal?.aborted) {
@@ -132,7 +132,7 @@ export const ServerFunctionsProvider: React.FC<{
if (!remoteSignal?.aborted) {
const result = (await serverFunction({
name: 'render-document',
args: rest,
args: { fallbackLocale: false, ...rest },
})) as { docID: string; Document: React.ReactNode }
if (!remoteSignal?.aborted) {

View File

@@ -38,12 +38,21 @@ const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
describe('Localization', () => {
beforeAll(async () => {
;({ payload, restClient } = await initPayloadInt(dirname))
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy()
}
})
describe('Localization with fallback true', () => {
let post1: LocalizedPost
let postWithLocalizedData: LocalizedPost
beforeAll(async () => {
;({ payload, restClient } = await initPayloadInt(dirname))
post1 = await payload.create({
collection,
data: {
@@ -68,12 +77,6 @@ describe('Localization', () => {
})
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy()
}
})
describe('Localized text', () => {
it('create english', async () => {
const allDocs = await payload.find({
@@ -134,6 +137,28 @@ describe('Localization', () => {
expect(localizedFallback.title.es).toEqual('')
})
it('should fallback to spanish translation when empty and locale-specific fallback is provided', async () => {
const localizedFallback: any = await payload.findByID({
id: postWithLocalizedData.id,
collection,
locale: portugueseLocale,
})
expect(localizedFallback.title).toEqual(spanishTitle)
})
it('should respect fallback none', async () => {
const localizedFallback: any = await payload.findByID({
id: postWithLocalizedData.id,
collection,
locale: portugueseLocale,
// @ts-expect-error - testing fallbackLocale 'none' for backwards compatibility though the correct type here is `false`
fallbackLocale: 'none',
})
expect(localizedFallback.title).not.toBeDefined()
})
describe('fallback locales', () => {
let englishData
let spanishData
@@ -419,12 +444,17 @@ describe('Localization', () => {
sort: 'date',
})
console.log({ sortByTitleQuery })
expect(sortByTitleQuery.totalDocs).toEqual(expectedTotalDocs)
expect(sortByDateQuery.totalDocs).toEqual(expectedTotalDocs)
})
it('should return correct order when sorted by localized fields', async () => {
const { docs: docsAsc } = await payload.find({ collection: localizedSortSlug, sort: 'title' })
const { docs: docsAsc } = await payload.find({
collection: localizedSortSlug,
sort: 'title',
})
docsAsc.forEach((doc, i) => {
expect(posts[i].id).toBe(doc.id)
})
@@ -690,7 +720,9 @@ describe('Localization', () => {
depth: 1,
locale: spanishLocale,
})
expect((result.localizedRelationship as LocalizedPost).title).toEqual(relationSpanishTitle)
expect((result.localizedRelationship as LocalizedPost).title).toEqual(
relationSpanishTitle,
)
})
it('all locales', async () => {
@@ -1088,7 +1120,7 @@ describe('Localization', () => {
data: {
items: [],
},
fallbackLocale: null,
fallbackLocale: 'none',
locale: spanishLocale,
})
@@ -2421,6 +2453,117 @@ describe('Localization', () => {
})
})
describe('Localization with fallback false', () => {
let post1: LocalizedPost
let postWithLocalizedData: LocalizedPost
beforeAll(async () => {
if (payload.config.localization) {
payload.config.localization.fallback = false
}
post1 = await payload.create({
collection,
data: {
title: englishTitle,
},
})
postWithLocalizedData = await payload.create({
collection,
data: {
title: englishTitle,
},
})
await payload.update({
id: postWithLocalizedData.id,
collection,
data: {
title: spanishTitle,
},
locale: spanishLocale,
})
})
describe('fallback locale', () => {
it('create english', async () => {
const allDocs = await payload.find({
collection,
where: {
title: { equals: post1.title },
},
})
expect(allDocs.docs).toContainEqual(expect.objectContaining(post1))
})
it('add spanish translation', async () => {
const updated = await payload.update({
id: post1.id,
collection,
data: {
title: spanishTitle,
},
locale: spanishLocale,
})
expect(updated.title).toEqual(spanishTitle)
const localized: any = await payload.findByID({
id: post1.id,
collection,
locale: 'all',
})
expect(localized.title.en).toEqual(englishTitle)
expect(localized.title.es).toEqual(spanishTitle)
})
it('should not fallback to english', async () => {
const retrievedDoc = await payload.findByID({
id: post1.id,
collection,
locale: portugueseLocale,
})
expect(retrievedDoc.title).not.toBeDefined()
})
it('should fallback to english with explicit fallbackLocale', async () => {
const fallbackDoc = await payload.findByID({
id: post1.id,
collection,
locale: portugueseLocale,
fallbackLocale: englishLocale,
})
expect(fallbackDoc.title).toBe(englishTitle)
})
it('should not fallback to spanish translation and no explicit fallback is provided', async () => {
const localizedFallback: any = await payload.findByID({
id: postWithLocalizedData.id,
collection,
locale: portugueseLocale,
})
expect(localizedFallback.title).not.toBeDefined()
})
it('should respect fallback none', async () => {
const localizedFallback: any = await payload.findByID({
id: postWithLocalizedData.id,
collection,
locale: portugueseLocale,
fallbackLocale: false,
})
expect(localizedFallback.title).not.toBeDefined()
})
})
})
})
async function createLocalizedPost(data: {
title: {
[defaultLocale]: string