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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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`,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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,14 +18,16 @@ 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'] &&
|
|
||||||
typeof data?.['fallback-locale'] === 'string'
|
|
||||||
) {
|
|
||||||
fallbackLocaleOnReq = data['fallback-locale']
|
fallbackLocaleOnReq = data['fallback-locale']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data?.['fallbackLocale'] && typeof data?.['fallbackLocale'] === 'string') {
|
||||||
|
fallbackLocaleOnReq = data['fallbackLocale']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { fallbackLocale, locale } = sanitizeLocales({
|
const { fallbackLocale, locale } = sanitizeLocales({
|
||||||
fallbackLocale: fallbackLocaleOnReq,
|
fallbackLocale: fallbackLocaleOnReq,
|
||||||
locale: localeOnReq,
|
locale: localeOnReq,
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'] = {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
53
packages/payload/src/utilities/sanitizeFallbackLocale.ts
Normal file
53
packages/payload/src/utilities/sanitizeFallbackLocale.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -38,12 +38,21 @@ const filename = fileURLToPath(import.meta.url)
|
|||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
describe('Localization', () => {
|
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 post1: LocalizedPost
|
||||||
let postWithLocalizedData: LocalizedPost
|
let postWithLocalizedData: LocalizedPost
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
|
||||||
|
|
||||||
post1 = await payload.create({
|
post1 = await payload.create({
|
||||||
collection,
|
collection,
|
||||||
data: {
|
data: {
|
||||||
@@ -68,12 +77,6 @@ describe('Localization', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
if (typeof payload.db.destroy === 'function') {
|
|
||||||
await payload.db.destroy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Localized text', () => {
|
describe('Localized text', () => {
|
||||||
it('create english', async () => {
|
it('create english', async () => {
|
||||||
const allDocs = await payload.find({
|
const allDocs = await payload.find({
|
||||||
@@ -134,6 +137,28 @@ describe('Localization', () => {
|
|||||||
expect(localizedFallback.title.es).toEqual('')
|
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', () => {
|
describe('fallback locales', () => {
|
||||||
let englishData
|
let englishData
|
||||||
let spanishData
|
let spanishData
|
||||||
@@ -419,12 +444,17 @@ describe('Localization', () => {
|
|||||||
sort: 'date',
|
sort: 'date',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log({ sortByTitleQuery })
|
||||||
|
|
||||||
expect(sortByTitleQuery.totalDocs).toEqual(expectedTotalDocs)
|
expect(sortByTitleQuery.totalDocs).toEqual(expectedTotalDocs)
|
||||||
expect(sortByDateQuery.totalDocs).toEqual(expectedTotalDocs)
|
expect(sortByDateQuery.totalDocs).toEqual(expectedTotalDocs)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return correct order when sorted by localized fields', async () => {
|
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) => {
|
docsAsc.forEach((doc, i) => {
|
||||||
expect(posts[i].id).toBe(doc.id)
|
expect(posts[i].id).toBe(doc.id)
|
||||||
})
|
})
|
||||||
@@ -690,7 +720,9 @@ describe('Localization', () => {
|
|||||||
depth: 1,
|
depth: 1,
|
||||||
locale: spanishLocale,
|
locale: spanishLocale,
|
||||||
})
|
})
|
||||||
expect((result.localizedRelationship as LocalizedPost).title).toEqual(relationSpanishTitle)
|
expect((result.localizedRelationship as LocalizedPost).title).toEqual(
|
||||||
|
relationSpanishTitle,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('all locales', async () => {
|
it('all locales', async () => {
|
||||||
@@ -1088,7 +1120,7 @@ describe('Localization', () => {
|
|||||||
data: {
|
data: {
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
fallbackLocale: null,
|
fallbackLocale: 'none',
|
||||||
locale: spanishLocale,
|
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: {
|
async function createLocalizedPost(data: {
|
||||||
title: {
|
title: {
|
||||||
[defaultLocale]: string
|
[defaultLocale]: string
|
||||||
|
|||||||
Reference in New Issue
Block a user