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 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) | | **`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. | | **`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 ### Locales

View File

@@ -20,7 +20,9 @@ export const Login = ({ tenantSlug }: Props) => {
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
e.preventDefault() e.preventDefault()
if (!usernameRef?.current?.value || !passwordRef?.current?.value) {return} if (!usernameRef?.current?.value || !passwordRef?.current?.value) {
return
}
const actionRes = await fetch( const actionRes = await fetch(
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/external-users/login`, `${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 }) => { export const ensureUniqueSlug: FieldHook = async ({ data, originalDoc, req, value }) => {
// if value is unchanged, skip validation // 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 incomingTenantID = typeof data?.tenant === 'object' ? data.tenant.id : data?.tenant
const currentTenantID = const currentTenantID =

View File

@@ -14,7 +14,9 @@ export const Pages: CollectionConfig = {
read: (args) => { read: (args) => {
// when viewing pages inside the admin panel // when viewing pages inside the admin panel
// restrict access to the ones your user has access to // 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 // when viewing pages from outside the admin panel
// you should be able to see your tenants and public tenants // 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 const req = args.req
// Super admin can read all // Super admin can read all
if (isSuperAdmin(args)) {return true} if (isSuperAdmin(args)) {
return true
}
const tenantIDs = getTenantAccessIDs(req.user) const tenantIDs = getTenantAccessIDs(req.user)

View File

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

View File

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

View File

@@ -5,9 +5,13 @@ import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAcces
export const updateAndDeleteAccess: Access = (args) => { export const updateAndDeleteAccess: Access = (args) => {
const { req } = 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) 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 }) => { export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req, value }) => {
// if value is unchanged, skip validation // 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 incomingTenantID = typeof data?.tenant === 'object' ? data.tenant.id : data?.tenant
const currentTenantID = const currentTenantID =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -444,7 +444,11 @@ export type BaseLocalizationConfig = {
* @example `"en"` * @example `"en"`
*/ */
defaultLocale: string 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 fallback?: boolean
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
import type { User } from '../auth/types.js' 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 type { PayloadRequest } from '../types/index.js'
import { getDataLoader } from '../collections/dataloader.js' import { getDataLoader } from '../collections/dataloader.js'
import { getLocalI18n } from '../translations/getLocalI18n.js' import { getLocalI18n } from '../translations/getLocalI18n.js'
import { sanitizeFallbackLocale } from '../utilities/sanitizeFallbackLocale.js'
function getRequestContext( function getRequestContext(
req: Partial<PayloadRequest> = { context: null } as PayloadRequest, req: Partial<PayloadRequest> = { context: null } as PayloadRequest,
@@ -71,7 +72,7 @@ const attachFakeURLProperties = (req: Partial<PayloadRequest>) => {
type CreateLocalReq = ( type CreateLocalReq = (
options: { options: {
context?: RequestContext context?: RequestContext
fallbackLocale?: string fallbackLocale?: false | TypedLocale
locale?: string locale?: string
req?: Partial<PayloadRequest> req?: Partial<PayloadRequest>
user?: User user?: User
@@ -83,23 +84,22 @@ export const createLocalReq: CreateLocalReq = async (
{ context, fallbackLocale, locale: localeArg, req = {} as PayloadRequest, user }, { context, fallbackLocale, locale: localeArg, req = {} as PayloadRequest, user },
payload, payload,
) => { ) => {
if (payload.config?.localization) { const localization = payload.config?.localization
if (localization) {
const locale = localeArg === '*' ? 'all' : localeArg const locale = localeArg === '*' ? 'all' : localeArg
const defaultLocale = payload.config.localization.defaultLocale const defaultLocale = localization.defaultLocale
const localeCandidate = locale || req?.locale || req?.query?.locale const localeCandidate = locale || req?.locale || req?.query?.locale
req.locale = req.locale =
localeCandidate && typeof localeCandidate === 'string' ? localeCandidate : defaultLocale localeCandidate && typeof localeCandidate === 'string' ? localeCandidate : defaultLocale
const fallbackLocaleFromConfig = payload.config.localization.locales.find( const sanitizedFallback = sanitizeFallbackLocale({
({ code }) => req.locale === code, fallbackLocale,
)?.fallbackLocale locale: req.locale,
localization,
})
if (typeof fallbackLocale !== 'undefined') { req.fallbackLocale = sanitizedFallback
req.fallbackLocale = fallbackLocale
} else if (typeof req?.fallbackLocale === 'undefined') {
req.fallbackLocale = fallbackLocaleFromConfig || defaultLocale
}
} }
const i18n = 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) { if (!remoteSignal?.aborted) {
const result = (await serverFunction({ const result = (await serverFunction({
name: 'form-state', name: 'form-state',
args: rest, args: { fallbackLocale: false, ...rest },
})) as ReturnType<typeof buildFormStateHandler> // TODO: infer this type when `strictNullChecks` is enabled })) as ReturnType<typeof buildFormStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
if (!remoteSignal?.aborted) { if (!remoteSignal?.aborted) {
@@ -108,7 +108,7 @@ export const ServerFunctionsProvider: React.FC<{
if (!remoteSignal?.aborted) { if (!remoteSignal?.aborted) {
const result = (await serverFunction({ const result = (await serverFunction({
name: 'table-state', name: 'table-state',
args: rest, args: { fallbackLocale: false, ...rest },
})) as ReturnType<typeof buildTableStateHandler> // TODO: infer this type when `strictNullChecks` is enabled })) as ReturnType<typeof buildTableStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
if (!remoteSignal?.aborted) { if (!remoteSignal?.aborted) {
@@ -132,7 +132,7 @@ export const ServerFunctionsProvider: React.FC<{
if (!remoteSignal?.aborted) { if (!remoteSignal?.aborted) {
const result = (await serverFunction({ const result = (await serverFunction({
name: 'render-document', name: 'render-document',
args: rest, args: { fallbackLocale: false, ...rest },
})) as { docID: string; Document: React.ReactNode } })) as { docID: string; Document: React.ReactNode }
if (!remoteSignal?.aborted) { if (!remoteSignal?.aborted) {

File diff suppressed because it is too large Load Diff