import type { Payload, User, Where } from 'payload' import path from 'path' import { createLocalReq } from 'payload' import { fileURLToPath } from 'url' import type { NextRESTClient } from '../helpers/NextRESTClient.js' import type { LocalizedPost, LocalizedSort, Nested, WithLocalizedRelationship, } from './payload-types.js' import { devUser } from '../credentials.js' // eslint-disable-next-line payload/no-relative-monorepo-imports import { copyDataFromLocaleHandler } from '../../packages/ui/src/utilities/copyDataFromLocale.js' import { idToString } from '../helpers/idToString.js' import { initPayloadInt } from '../helpers/initPayloadInt.js' import { arrayCollectionSlug } from './collections/Array/index.js' import { groupSlug } from './collections/Group/index.js' import { nestedToArrayAndBlockCollectionSlug } from './collections/NestedToArrayAndBlock/index.js' import { tabSlug } from './collections/Tab/index.js' import { defaultLocale, defaultLocale as englishLocale, englishTitle, hungarianLocale, localizedDateFieldsSlug, localizedPostsSlug, localizedSortSlug, portugueseLocale, relationEnglishTitle, relationEnglishTitle2, relationshipLocalizedSlug, relationSpanishTitle, relationSpanishTitle2, spanishLocale, spanishTitle, withLocalizedRelSlug, withRequiredLocalizedFields, } from './shared.js' const collection = localizedPostsSlug let payload: Payload let restClient: NextRESTClient 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 () => { 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('Localized text', () => { 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 fallback to english translation when empty', async () => { await payload.update({ id: post1.id, collection, data: { title: '', }, locale: spanishLocale, }) const retrievedInEnglish = await payload.findByID({ id: post1.id, collection, }) expect(retrievedInEnglish.title).toEqual(englishTitle) const localizedFallback: any = await payload.findByID({ id: post1.id, collection, locale: 'all', }) expect(localizedFallback.title.en).toEqual(englishTitle) 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 let localizedDoc beforeAll(async () => { englishData = { localizedCheckbox: false, } spanishData = { localizedCheckbox: true, title: 'spanish title', } localizedDoc = await payload.create({ collection: localizedPostsSlug, data: englishData, locale: englishLocale, }) await payload.update({ id: localizedDoc.id, collection: localizedPostsSlug, data: spanishData, locale: spanishLocale, }) await payload.update({ id: localizedDoc.id, collection: localizedPostsSlug, data: { localizedCheckbox: true }, locale: portugueseLocale, }) }) it('should return localized fields using fallbackLocale specified in the requested locale config', async () => { const portugueseDoc = await payload.findByID({ id: localizedDoc.id, collection: localizedPostsSlug, locale: portugueseLocale, }) expect(portugueseDoc.title).toStrictEqual(spanishData.title) expect(portugueseDoc.localizedCheckbox).toStrictEqual(true) }) }) describe('querying', () => { let localizedPost: LocalizedPost beforeEach(async () => { const { id } = await payload.create({ collection, data: { title: englishTitle, }, }) localizedPost = await payload.update({ id, collection, data: { title: spanishTitle, }, locale: spanishLocale, }) }) it('unspecified locale returns default', async () => { const localized = await payload.findByID({ id: localizedPost.id, collection, }) expect(localized.title).toEqual(englishTitle) }) it('specific locale - same as default', async () => { const localized = await payload.findByID({ id: localizedPost.id, collection, locale: defaultLocale, }) expect(localized.title).toEqual(englishTitle) }) it('specific locale - not default', async () => { const localized = await payload.findByID({ id: localizedPost.id, collection, locale: spanishLocale, }) expect(localized.title).toEqual(spanishTitle) }) it('all locales', async () => { const localized: any = await payload.findByID({ id: localizedPost.id, collection, locale: 'all', }) expect(localized.title.en).toEqual(englishTitle) expect(localized.title.es).toEqual(spanishTitle) }) it('rest all locales with all', async () => { const response = await restClient.GET(`/${collection}/${localizedPost.id}`, { query: { locale: 'all', }, }) expect(response.status).toBe(200) const localized = await response.json() expect(localized.title.en).toEqual(englishTitle) expect(localized.title.es).toEqual(spanishTitle) }) it('rest all locales with asterisk', async () => { const response = await restClient.GET(`/${collection}/${localizedPost.id}`, { query: { locale: '*', }, }) expect(response.status).toBe(200) const localized = await response.json() expect(localized.title.en).toEqual(englishTitle) expect(localized.title.es).toEqual(spanishTitle) }) it('by localized field value - default locale', async () => { const result = await payload.find({ collection, where: { title: { equals: englishTitle, }, }, }) expect(result.docs.map(({ id }) => id)).toContain(localizedPost.id) }) it('by localized field value - alternate locale', async () => { const result = await payload.find({ collection, locale: spanishLocale, where: { title: { equals: spanishTitle, }, }, }) expect(result.docs.map(({ id }) => id)).toContain(localizedPost.id) }) it('by localized field value - opposite locale???', async () => { const result = await payload.find({ collection, locale: 'all', where: { 'title.es': { equals: spanishTitle, }, }, }) expect(result.docs.map(({ id }) => id)).toContain(localizedPost.id) }) it('by localized field value with sorting', async () => { const doc_1 = await payload.create({ collection, data: { title: 'word_b' } }) const doc_2 = await payload.create({ collection, data: { title: 'word_a' } }) const doc_3 = await payload.create({ collection, data: { title: 'word_c' } }) await payload.create({ collection, data: { title: 'others_c' } }) const { docs } = await payload.find({ collection, sort: 'title', where: { title: { like: 'word', }, }, }) expect(docs).toHaveLength(3) expect(docs[0].id).toBe(doc_2.id) expect(docs[1].id).toBe(doc_1.id) expect(docs[2].id).toBe(doc_3.id) }) if (['mongodb'].includes(process.env.PAYLOAD_DATABASE)) { describe('Localized sorting', () => { let localizedAccentPostOne: LocalizedPost let localizedAccentPostTwo: LocalizedPost beforeEach(async () => { localizedAccentPostOne = await payload.create({ collection, data: { title: 'non accent post', localizedDescription: 'something', }, locale: englishLocale, }) localizedAccentPostTwo = await payload.create({ collection, data: { title: 'accent post', localizedDescription: 'veterinarian', }, locale: englishLocale, }) await payload.update({ id: localizedAccentPostOne.id, collection, data: { title: 'non accent post', localizedDescription: 'valami', }, locale: hungarianLocale, }) await payload.update({ id: localizedAccentPostTwo.id, collection, data: { title: 'accent post', localizedDescription: 'állatorvos', }, locale: hungarianLocale, }) }) it('should sort alphabetically even with accented letters', async () => { const sortByDescriptionQuery = await payload.find({ collection, sort: 'description', where: { title: { like: 'accent', }, }, locale: hungarianLocale, }) expect(sortByDescriptionQuery.docs[0].id).toEqual(localizedAccentPostTwo.id) }) }) } }) }) describe('Localized date', () => { it('can create a localized date', async () => { const document = await payload.create({ collection: localizedDateFieldsSlug, data: { localizedDate: new Date().toISOString(), date: new Date().toISOString(), }, }) expect(document.localizedDate).toBeTruthy() }) it('data is typed as string', async () => { const document = await payload.create({ collection: localizedDateFieldsSlug, data: { localizedDate: new Date().toISOString(), date: new Date().toISOString(), }, }) expect(typeof document.localizedDate).toBe('string') expect(typeof document.date).toBe('string') }) }) describe('Localized Sort Count', () => { const expectedTotalDocs = 5 const posts: LocalizedSort[] = [] beforeAll(async () => { for (let i = 1; i <= expectedTotalDocs; i++) { const post = await payload.create({ collection: localizedSortSlug, data: { date: new Date().toISOString(), title: `EN ${i}`, }, locale: englishLocale, }) posts.push(post) await payload.update({ id: post.id, collection: localizedSortSlug, data: { date: new Date().toISOString(), title: `ES ${i}`, }, locale: spanishLocale, }) } }) it('should have correct totalDocs when unsorted', async () => { const simpleQuery = await payload.find({ collection: localizedSortSlug, }) const sortByIdQuery = await payload.find({ collection: localizedSortSlug, sort: 'id', }) expect(simpleQuery.totalDocs).toEqual(expectedTotalDocs) expect(sortByIdQuery.totalDocs).toEqual(expectedTotalDocs) }) // https://github.com/payloadcms/payload/issues/4889 it('should have correct totalDocs when sorted by localized fields', async () => { const sortByTitleQuery = await payload.find({ collection: localizedSortSlug, sort: 'title', }) const sortByDateQuery = await payload.find({ collection: localizedSortSlug, 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', }) docsAsc.forEach((doc, i) => { expect(posts[i].id).toBe(doc.id) }) const { docs: docsDesc } = await payload.find({ collection: localizedSortSlug, sort: '-title', }) docsDesc.forEach((doc, i) => { expect(posts.at(posts.length - i - 1).id).toBe(doc.id) }) // Test with words const randomWords = [ 'sunset', 'whisper', 'lighthouse', 'harmony', 'crystal', 'thunder', 'meadow', 'voyage', 'echo', 'quicksand', ] const randomWordsSpanish = [ 'atardecer', 'susurro', 'faro', 'armonía', 'cristal', 'trueno', 'pradera', 'viaje', 'eco', 'arenas movedizas', ] expect(randomWords).toHaveLength(randomWordsSpanish.length) const randomWordsPosts: (number | string)[] = [] for (let i = 0; i < randomWords.length; i++) { const en = randomWords[i] const post = await payload.create({ collection: 'localized-sort', data: { title: en } }) const es = randomWordsSpanish[i] await payload.update({ collection: 'localized-sort', data: { title: es }, id: post.id, locale: 'es', }) randomWordsPosts.push(post.id) } const ascSortedWordsEn = randomWords.toSorted((a, b) => a.localeCompare(b)) const descSortedWordsEn = randomWords.toSorted((a, b) => b.localeCompare(a)) const q = { id: { in: randomWordsPosts } } const { docs: randomWordsEnAsc } = await payload.find({ collection: localizedSortSlug, sort: 'title', where: q, }) randomWordsEnAsc.forEach((doc, i) => { expect(ascSortedWordsEn[i]).toBe(doc.title) }) const { docs: randomWordsEnDesc } = await payload.find({ collection: localizedSortSlug, sort: '-title', where: q, }) randomWordsEnDesc.forEach((doc, i) => { expect(descSortedWordsEn[i]).toBe(doc.title) }) // Test sorting for Spanish locale const ascSortedWordsEs = randomWordsSpanish.toSorted((a, b) => a.localeCompare(b)) const descSortedWordsEs = randomWordsSpanish.toSorted((a, b) => b.localeCompare(a)) // Fetch sorted words in Spanish (ascending) const { docs: randomWordsEsAsc } = await payload.find({ collection: localizedSortSlug, sort: 'title', where: q, locale: 'es', }) randomWordsEsAsc.forEach((doc, i) => { expect(ascSortedWordsEs[i]).toBe(doc.title) }) // Fetch sorted words in Spanish (descending) const { docs: randomWordsEsDesc } = await payload.find({ collection: localizedSortSlug, sort: '-title', where: q, locale: 'es', }) randomWordsEsDesc.forEach((doc, i) => { expect(descSortedWordsEs[i]).toBe(doc.title) }) }) }) describe('Localized Relationship', () => { let localizedRelation: LocalizedPost let localizedRelation2: LocalizedPost let withRelationship: WithLocalizedRelationship beforeAll(async () => { localizedRelation = await createLocalizedPost({ title: { [defaultLocale]: relationEnglishTitle, [spanishLocale]: relationSpanishTitle, }, }) localizedRelation2 = await createLocalizedPost({ title: { [defaultLocale]: relationEnglishTitle2, [spanishLocale]: relationSpanishTitle2, }, }) withRelationship = await payload.create({ collection: withLocalizedRelSlug, data: { localizedRelationHasManyField: [localizedRelation.id, localizedRelation2.id], localizedRelationMultiRelationTo: { relationTo: localizedPostsSlug, value: localizedRelation.id, }, localizedRelationMultiRelationToHasMany: [ { relationTo: localizedPostsSlug, value: localizedRelation.id }, { relationTo: localizedPostsSlug, value: localizedRelation2.id }, ], localizedRelationship: localizedRelation.id, }, }) }) describe('regular relationship', () => { it('can query localized relationship', async () => { const result = await payload.find({ collection: withLocalizedRelSlug, where: { 'localizedRelationship.title': { equals: localizedRelation.title, }, }, }) expect(result.docs[0].id).toEqual(withRelationship.id) }) it('specific locale', async () => { const result = await payload.find({ collection: withLocalizedRelSlug, locale: spanishLocale, where: { 'localizedRelationship.title': { equals: relationSpanishTitle, }, }, }) expect(result.docs[0].id).toEqual(withRelationship.id) }) it('all locales', async () => { const result = await payload.find({ collection: withLocalizedRelSlug, locale: 'all', where: { 'localizedRelationship.title.es': { equals: relationSpanishTitle, }, }, }) expect(result.docs[0].id).toEqual(withRelationship.id) }) it('populates relationships with all locales', async () => { // the relationship fields themselves are localized on this collection const result: any = await payload.find({ collection: relationshipLocalizedSlug, depth: 1, locale: 'all', }) expect(result.docs[0].relationship.en.id).toBeDefined() expect(result.docs[0].relationshipHasMany.en[0].id).toBeDefined() expect(result.docs[0].relationMultiRelationTo.en.value.id).toBeDefined() expect(result.docs[0].relationMultiRelationToHasMany.en[0].value.id).toBeDefined() expect(result.docs[0].arrayField.en[0].nestedRelation.id).toBeDefined() }) }) describe('relationship - hasMany', () => { it('default locale', async () => { const result = await payload.find({ collection: withLocalizedRelSlug, where: { 'localizedRelationHasManyField.title': { equals: localizedRelation.title, }, }, }) expect(result.docs.map(({ id }) => id)).toContain(withRelationship.id) // Second relationship const result2 = await payload.find({ collection: withLocalizedRelSlug, where: { 'localizedRelationHasManyField.title': { equals: localizedRelation2.title, }, }, }) expect(result2.docs.map(({ id }) => id)).toContain(withRelationship.id) }) it('specific locale', async () => { const result = await payload.find({ collection: withLocalizedRelSlug, locale: spanishLocale, where: { 'localizedRelationHasManyField.title': { equals: relationSpanishTitle, }, }, }) expect(result.docs[0].id).toEqual(withRelationship.id) // Second relationship const result2 = await payload.find({ collection: withLocalizedRelSlug, locale: spanishLocale, where: { 'localizedRelationHasManyField.title': { equals: relationSpanishTitle2, }, }, }) expect(result2.docs[0].id).toEqual(withRelationship.id) }) it('relationship population uses locale', async () => { const result = await payload.findByID({ id: withRelationship.id, collection: withLocalizedRelSlug, depth: 1, locale: spanishLocale, }) expect((result.localizedRelationship as LocalizedPost).title).toEqual( relationSpanishTitle, ) }) it('all locales', async () => { const queryRelation = (where: Where) => { return payload.find({ collection: withLocalizedRelSlug, locale: 'all', where, }) } const result = await queryRelation({ 'localizedRelationHasManyField.title.en': { equals: relationEnglishTitle, }, }) expect(result.docs.map(({ id }) => id)).toContain(withRelationship.id) // First relationship - spanish const result2 = await queryRelation({ 'localizedRelationHasManyField.title.es': { equals: relationSpanishTitle, }, }) expect(result2.docs.map(({ id }) => id)).toContain(withRelationship.id) // Second relationship - english const result3 = await queryRelation({ 'localizedRelationHasManyField.title.en': { equals: relationEnglishTitle2, }, }) expect(result3.docs.map(({ id }) => id)).toContain(withRelationship.id) // Second relationship - spanish const result4 = await queryRelation({ 'localizedRelationHasManyField.title.es': { equals: relationSpanishTitle2, }, }) expect(result4.docs[0].id).toEqual(withRelationship.id) }) }) describe('relationTo multi', () => { it('by id', async () => { const result = await payload.find({ collection: withLocalizedRelSlug, where: { 'localizedRelationMultiRelationTo.value': { equals: localizedRelation.id, }, }, }) expect(result.docs[0].id).toEqual(withRelationship.id) // Second relationship const result2 = await payload.find({ collection: withLocalizedRelSlug, locale: spanishLocale, where: { 'localizedRelationMultiRelationTo.value': { equals: localizedRelation.id, }, }, }) expect(result2.docs[0].id).toEqual(withRelationship.id) }) }) describe('relationTo multi hasMany', () => { it('by id', async () => { const result = await payload.find({ collection: withLocalizedRelSlug, where: { 'localizedRelationMultiRelationToHasMany.value': { equals: localizedRelation.id, }, }, }) expect(result.docs[0].id).toEqual(withRelationship.id) // First relationship - spanish locale const result2 = await payload.find({ collection: withLocalizedRelSlug, locale: spanishLocale, where: { 'localizedRelationMultiRelationToHasMany.value': { equals: localizedRelation.id, }, }, }) expect(result2.docs[0].id).toEqual(withRelationship.id) // Second relationship const result3 = await payload.find({ collection: withLocalizedRelSlug, where: { 'localizedRelationMultiRelationToHasMany.value': { equals: localizedRelation2.id, }, }, }) expect(result3.docs[0].id).toEqual(withRelationship.id) // Second relationship - spanish locale const result4 = await payload.find({ collection: withLocalizedRelSlug, where: { 'localizedRelationMultiRelationToHasMany.value': { equals: localizedRelation2.id, }, }, }) expect(result4.docs[0].id).toEqual(withRelationship.id) }) }) }) describe('Localized - arrays with nested localized fields', () => { it('should allow moving rows and retain existing row locale data', async () => { const globalArray: any = await payload.findGlobal({ slug: 'global-array', }) const reversedArrayRows = [...globalArray.array].reverse() const updatedGlobal = await payload.updateGlobal({ slug: 'global-array', data: { array: reversedArrayRows, }, locale: 'all', }) expect(updatedGlobal.array[0].text.en).toStrictEqual('test en 2') expect(updatedGlobal.array[0].text.es).toStrictEqual('test es 2') }) }) describe('Localized - required', () => { it('should update without passing all required fields', async () => { const newDoc = await payload.create({ collection: withRequiredLocalizedFields, data: { nav: { layout: [ { blockType: 'text', text: 'laiwejfilwaje', }, ], }, title: 'hello', }, }) await payload.update({ id: newDoc.id, collection: withRequiredLocalizedFields, data: { nav: { layout: [ { blockType: 'number', number: 12, }, ], }, title: 'en espanol, big bird', }, locale: spanishLocale, }) const updatedDoc = await payload.update({ id: newDoc.id, collection: withRequiredLocalizedFields, data: { title: 'hello x2', }, }) expect(updatedDoc.nav.layout[0].blockType).toStrictEqual('text') const spanishDoc = await payload.findByID({ id: newDoc.id, collection: withRequiredLocalizedFields, locale: spanishLocale, }) expect(spanishDoc.nav.layout[0].blockType).toStrictEqual('number') }) }) describe('Localized - GraphQL', () => { let token let client it('should allow user to login and retrieve populated localized field', async () => { const query = `mutation { loginUser(email: "dev@payloadcms.com", password: "test") { token user { relation { title } } } }` const { data } = await restClient .GRAPHQL_POST({ body: JSON.stringify({ query }), query: { locale: 'en' }, }) .then((res) => res.json()) const result = data.loginUser expect(typeof result.token).toStrictEqual('string') expect(typeof result.user.relation.title).toStrictEqual('string') token = result.token }) it('should allow retrieval of populated localized fields within meUser', async () => { const query = `query { meUser { user { id relation { title } } } }` const { data } = await restClient .GRAPHQL_POST({ body: JSON.stringify({ query }), headers: { Authorization: `JWT ${token}`, }, query: { locale: 'en' }, }) .then((res) => res.json()) const result = data.meUser expect(typeof result.user.relation.title).toStrictEqual('string') }) it('should create and update collections', async () => { const create = `mutation { createLocalizedPost( data: { title: "${englishTitle}" } locale: ${defaultLocale} ) { id title } }` const { data } = await restClient .GRAPHQL_POST({ body: JSON.stringify({ query: create }), headers: { Authorization: `JWT ${token}`, }, query: { locale: 'en' }, }) .then((res) => res.json()) const createResult = data.createLocalizedPost const update = `mutation { updateLocalizedPost( id: ${payload.db.defaultIDType === 'number' ? createResult.id : `"${createResult.id}"`}, data: { title: "${spanishTitle}" } locale: ${spanishLocale} ) { title } }` const { data: updateData } = await restClient .GRAPHQL_POST({ body: JSON.stringify({ query: update }), headers: { Authorization: `JWT ${token}`, }, query: { locale: 'en' }, }) .then((res) => res.json()) const updateResult = updateData.updateLocalizedPost const result = await payload.findByID({ id: createResult.id, collection: localizedPostsSlug, locale: 'all', }) expect(createResult.title).toStrictEqual(englishTitle) expect(updateResult.title).toStrictEqual(spanishTitle) expect(result.title[defaultLocale]).toStrictEqual(englishTitle) expect(result.title[spanishLocale]).toStrictEqual(spanishTitle) }) it('should query multiple locales', async () => { const englishDoc = await payload.create({ collection: localizedPostsSlug, data: { title: englishTitle, }, locale: defaultLocale, }) const spanishDoc = await payload.create({ collection: localizedPostsSlug, data: { title: spanishTitle, }, locale: spanishLocale, }) const query = ` { es: LocalizedPost(id: ${idToString(spanishDoc.id, payload)}, locale: es) { title } en: LocalizedPost(id: ${idToString(englishDoc.id, payload)}, locale: en) { title } } ` const { data: multipleLocaleData } = await restClient .GRAPHQL_POST({ body: JSON.stringify({ query }), headers: { Authorization: `JWT ${token}`, }, query: { locale: 'en' }, }) .then((res) => res.json()) const { en, es } = multipleLocaleData expect(en.title).toStrictEqual(englishTitle) expect(es.title).toStrictEqual(spanishTitle) }) }) describe('Localized - Arrays', () => { let docID beforeAll(async () => { const englishDoc = await payload.create({ collection: arrayCollectionSlug, data: { items: [ { text: englishTitle, }, ], }, }) docID = englishDoc.id }) it('should use default locale as fallback', async () => { const spanishDoc = await payload.findByID({ id: docID, collection: arrayCollectionSlug, locale: spanishLocale, }) expect(spanishDoc.items[0].text).toStrictEqual(englishTitle) }) it('should use empty array as value', async () => { const updatedSpanishDoc = await payload.update({ id: docID, collection: arrayCollectionSlug, data: { items: [], }, fallbackLocale: 'none', locale: spanishLocale, }) expect(updatedSpanishDoc.items).toStrictEqual([]) }) it('should use fallback value if setting null', async () => { await payload.update({ id: docID, collection: arrayCollectionSlug, data: { items: [], }, locale: spanishLocale, }) const updatedSpanishDoc = await payload.update({ id: docID, collection: arrayCollectionSlug, data: { items: null, }, locale: spanishLocale, }) // should return the value of the fallback locale expect(updatedSpanishDoc.items[0].text).toStrictEqual(englishTitle) }) }) describe('Localized - Field Paths', () => { it('should allow querying by non-localized field names ending in a locale', async () => { await payload.update({ id: post1.id, collection, data: { children: post1.id, group: { children: 'something', }, }, }) const { docs: relationshipDocs } = await restClient .GET(`/${collection}`, { query: { where: { children: { in: post1.id, }, }, }, }) .then((res) => res.json()) expect(relationshipDocs.map(({ id }) => id)).toContain(post1.id) const { docs: nestedFieldDocs } = await restClient .GET(`/${collection}`, { query: { where: { 'group.children': { contains: 'some', }, }, }, }) .then((res) => res.json()) expect(nestedFieldDocs.map(({ id }) => id)).toContain(post1.id) }) }) describe('Nested To Array And Block', () => { it('should be equal to the created document', async () => { const { id, blocks } = await payload.create({ collection: nestedToArrayAndBlockCollectionSlug, data: { blocks: [ { array: [ { text: 'english', textNotLocalized: 'test', }, ], blockType: 'block', }, ], }, locale: defaultLocale, }) await payload.update({ id, collection: nestedToArrayAndBlockCollectionSlug, data: { blocks: (blocks as { array: { text: string }[] }[]).map((block) => ({ ...block, array: block.array.map((item) => ({ ...item, text: 'spanish' })), })), }, locale: spanishLocale, }) const docDefaultLocale = await payload.findByID({ id, collection: nestedToArrayAndBlockCollectionSlug, locale: defaultLocale, }) const docSpanishLocale = await payload.findByID({ id, collection: nestedToArrayAndBlockCollectionSlug, locale: spanishLocale, }) const rowDefault = docDefaultLocale.blocks[0].array[0] const rowSpanish = docSpanishLocale.blocks[0].array[0] expect(rowDefault.text).toEqual('english') expect(rowDefault.textNotLocalized).toEqual('test') expect(rowSpanish.text).toEqual('spanish') expect(rowSpanish.textNotLocalized).toEqual('test') }) }) describe('Duplicate Collection', () => { it('should duplicate localized document', async () => { const localizedPost = await payload.create({ collection: localizedPostsSlug, data: { localizedCheckbox: true, title: englishTitle, }, locale: defaultLocale, }) const id = localizedPost.id.toString() await payload.update({ id, collection: localizedPostsSlug, data: { localizedCheckbox: false, title: spanishTitle, }, locale: spanishLocale, }) const result = await payload.duplicate({ id, collection: localizedPostsSlug, locale: defaultLocale, }) const allLocales = await payload.findByID({ id: result.id, collection: localizedPostsSlug, locale: 'all', }) // check fields expect(result.title).toStrictEqual(englishTitle) expect(allLocales.title.es).toStrictEqual(spanishTitle) expect(allLocales.localizedCheckbox.en).toBeTruthy() expect(allLocales.localizedCheckbox.es).toBeFalsy() }) it('should duplicate with localized blocks', async () => { // This test covers a few things: // 1. make sure we can duplicate localized blocks // - in relational DBs, we need to create new block / array IDs // - and this needs to be done recursively for all block / array fields // 2. make sure localized arrays / blocks work inside of localized groups / tabs // - this is covered with myTab.group.nestedArray2 // 3. the field schema for `nav` is within an unnamed tab, which tests that we // properly recursively loop through all field structures / types const englishText = 'english' const spanishText = 'spanish' const doc = await payload.create({ collection: withRequiredLocalizedFields, data: { nav: { layout: [ { blockType: 'text', text: englishText, nestedArray: [ { text: 'hello', l2: [ { l3: [ { l4: [ { superNestedText: 'hello', }, ], }, ], }, ], }, { text: 'goodbye', l2: [ { l3: [ { l4: [ { superNestedText: 'goodbye', }, ], }, ], }, ], }, ], }, ], }, myTab: { text: 'hello', group: { nestedText: 'hello', nestedArray2: [ { nestedText: 'hello', }, { nestedText: 'goodbye', }, ], }, }, title: 'hello', }, locale: defaultLocale, }) await payload.update({ id: doc.id, collection: withRequiredLocalizedFields, data: { nav: { layout: [ { blockType: 'text', text: spanishText, nestedArray: [ { text: 'hola', l2: [ { l3: [ { l4: [ { superNestedText: 'hola', }, ], }, ], }, ], }, { text: 'adios', l2: [ { l3: [ { l4: [ { superNestedText: 'adios', }, ], }, ], }, ], }, ], }, ], }, title: 'hello', myTab: { text: 'hola', group: { nestedText: 'hola', nestedArray2: [ { nestedText: 'hola', }, { nestedText: 'adios', }, ], }, }, }, locale: spanishLocale, }) const result = await payload.duplicate({ id: doc.id, collection: withRequiredLocalizedFields, locale: defaultLocale, }) const allLocales = await payload.findByID({ id: result.id, collection: withRequiredLocalizedFields, locale: 'all', }) // check fields expect(result.nav.layout[0].text).toStrictEqual(englishText) expect(allLocales.nav.layout.en[0].text).toStrictEqual(englishText) expect(allLocales.nav.layout.es[0].text).toStrictEqual(spanishText) expect(allLocales.myTab.group.en.nestedText).toStrictEqual('hello') expect(allLocales.myTab.group.en.nestedArray2[0].nestedText).toStrictEqual('hello') expect(allLocales.myTab.group.en.nestedArray2[1].nestedText).toStrictEqual('goodbye') expect(allLocales.myTab.group.es.nestedText).toStrictEqual('hola') expect(allLocales.myTab.group.es.nestedArray2[0].nestedText).toStrictEqual('hola') expect(allLocales.myTab.group.es.nestedArray2[1].nestedText).toStrictEqual('adios') }) }) describe('Localized group and tabs', () => { it('should properly create/update/read localized group field', async () => { const result = await payload.create({ collection: groupSlug, data: { groupLocalized: { title: 'hello en', }, }, locale: englishLocale, }) expect(result.groupLocalized?.title).toBe('hello en') await payload.update({ collection: groupSlug, locale: spanishLocale, id: result.id, data: { groupLocalized: { title: 'hello es', }, }, }) const docEn = await payload.findByID({ collection: groupSlug, locale: englishLocale, id: result.id, }) const docEs = await payload.findByID({ collection: groupSlug, locale: spanishLocale, id: result.id, }) expect(docEn.groupLocalized.title).toBe('hello en') expect(docEs.groupLocalized.title).toBe('hello es') }) it('should properly create/update/read localized field inside of group', async () => { const result = await payload.create({ collection: groupSlug, locale: englishLocale, data: { group: { title: 'hello en', }, }, }) expect(result.group.title).toBe('hello en') await payload.update({ collection: groupSlug, locale: spanishLocale, id: result.id, data: { group: { title: 'hello es', }, }, }) const docEn = await payload.findByID({ collection: groupSlug, locale: englishLocale, id: result.id, }) const docEs = await payload.findByID({ collection: groupSlug, locale: spanishLocale, id: result.id, }) expect(docEn.group.title).toBe('hello en') expect(docEs.group.title).toBe('hello es') }) it('should properly create/update/read deep localized field inside of group', async () => { const result = await payload.create({ collection: groupSlug, locale: englishLocale, data: { deep: { blocks: [ { blockType: 'first', title: 'hello en', }, ], array: [{ title: 'hello en' }], }, }, }) expect(result.deep.array[0].title).toBe('hello en') await payload.update({ collection: groupSlug, locale: spanishLocale, id: result.id, data: { deep: { blocks: [ { blockType: 'first', title: 'hello es', id: result.deep.blocks[0].id, }, ], array: [ { id: result.deep.array[0].id, title: 'hello es', }, ], }, }, }) const docEn = await payload.findByID({ collection: groupSlug, locale: englishLocale, id: result.id, }) const docEs = await payload.findByID({ collection: groupSlug, locale: spanishLocale, id: result.id, }) expect(docEn.deep.array[0].title).toBe('hello en') expect(docEn.deep.blocks[0].title).toBe('hello en') expect(docEs.deep.array[0].title).toBe('hello es') expect(docEs.deep.blocks[0].title).toBe('hello es') }) it('should create/updated/read localized group with row field', async () => { const doc = await payload.create({ collection: 'groups', data: { groupLocalizedRow: { text: 'hello world', }, }, locale: 'en', }) expect(doc.groupLocalizedRow.text).toBe('hello world') const docES = await payload.update({ collection: 'groups', data: { groupLocalizedRow: { text: 'hola world or something', }, }, locale: 'es', id: doc.id, }) expect(docES.groupLocalizedRow.text).toBe('hola world or something') // check if docES didnt break EN const docEN = await payload.findByID({ collection: 'groups', id: doc.id, locale: 'en' }) expect(docEN.groupLocalizedRow.text).toBe('hello world') const all = await payload.findByID({ collection: 'groups', id: doc.id, locale: 'all' }) expect(all.groupLocalizedRow.en.text).toBe('hello world') expect(all.groupLocalizedRow.es.text).toBe('hola world or something') }) it('should not crash on empty localized tab', async () => { const result = await payload.create({ collection: tabSlug, locale: englishLocale, data: { tabLocalized: {}, }, }) expect(result).toBeTruthy() }) it('should properly create/update/read array field inside localized tab field', async () => { const result = await payload.create({ collection: tabSlug, locale: englishLocale, data: { tabLocalized: { title: 'hello en', }, }, }) expect(result.tabLocalized?.title).toBe('hello en') await payload.update({ collection: tabSlug, locale: spanishLocale, id: result.id, data: { tabLocalized: { title: 'hello es', }, }, }) const docEn = await payload.findByID({ collection: tabSlug, locale: englishLocale, id: result.id, }) const docEs = await payload.findByID({ collection: tabSlug, locale: spanishLocale, id: result.id, }) expect(docEn.tabLocalized.title).toBe('hello en') expect(docEs.tabLocalized.title).toBe('hello es') }) it('should properly create/update/read localized tab field', async () => { const result = await payload.create({ collection: tabSlug, locale: englishLocale, data: { tabLocalized: { array: [ { title: 'hello en', }, ], }, }, }) expect(result.tabLocalized.array[0].title).toBe('hello en') await payload.update({ collection: tabSlug, locale: spanishLocale, id: result.id, data: { tabLocalized: { array: [{ title: 'hello es' }], }, }, }) const docEn = await payload.findByID({ collection: tabSlug, locale: englishLocale, id: result.id, }) const docEs = await payload.findByID({ collection: tabSlug, locale: spanishLocale, id: result.id, }) expect(docEn.tabLocalized.array[0].title).toBe('hello en') expect(docEs.tabLocalized.array[0].title).toBe('hello es') }) it('should properly create/update/read localized field inside of tab', async () => { const result = await payload.create({ collection: tabSlug, locale: englishLocale, data: { tab: { title: 'hello en', }, }, }) expect(result.tab.title).toBe('hello en') await payload.update({ collection: tabSlug, locale: spanishLocale, id: result.id, data: { tab: { title: 'hello es', }, }, }) const docEn = await payload.findByID({ collection: tabSlug, locale: englishLocale, id: result.id, }) const docEs = await payload.findByID({ collection: tabSlug, locale: spanishLocale, id: result.id, }) expect(docEn.tab.title).toBe('hello en') expect(docEs.tab.title).toBe('hello es') }) it('should properly create/update/read deep localized field inside of tab', async () => { const result = await payload.create({ collection: tabSlug, locale: englishLocale, data: { deep: { blocks: [ { blockType: 'first', title: 'hello en', }, ], array: [{ title: 'hello en' }], }, }, }) expect(result.deep.array[0].title).toBe('hello en') await payload.update({ collection: tabSlug, locale: spanishLocale, id: result.id, data: { deep: { blocks: [ { blockType: 'first', title: 'hello es', id: result.deep.blocks[0].id, }, ], array: [ { id: result.deep.array[0].id, title: 'hello es', }, ], }, }, }) const docEn = await payload.findByID({ collection: tabSlug, locale: englishLocale, id: result.id, }) const docEs = await payload.findByID({ collection: tabSlug, locale: spanishLocale, id: result.id, }) expect(docEn.deep.array[0].title).toBe('hello en') expect(docEn.deep.blocks[0].title).toBe('hello en') expect(docEs.deep.array[0].title).toBe('hello es') expect(docEs.deep.blocks[0].title).toBe('hello es') }) }) // Nested localized fields do no longer have their localized property stripped in // this monorepo, as this is handled at runtime. describe('nested localized field sanitization', () => { it('ensure nested localized fields keep localized property in monorepo', () => { const collection = payload.collections['localized-within-localized'].config expect(collection.fields[0].tabs[0].fields[0].localized).toBeDefined() expect(collection.fields[1].fields[0].localized).toBeDefined() expect(collection.fields[2].blocks[0].fields[0].localized).toBeDefined() expect(collection.fields[3].fields[0].localized).toBeDefined() }) }) describe('nested blocks', () => { let id it('should allow creating nested blocks per locale', async () => { const doc = await payload.create({ collection: 'blocks-fields', data: { content: [ { blockType: 'blockInsideBlock', array: [ { link: { label: 'English 1', }, }, { link: { label: 'English 2', }, }, ], content: [ { blockType: 'textBlock', text: 'hello', }, ], }, ], }, }) id = doc.id const retrievedInEN = await payload.findByID({ collection: 'blocks-fields', id, }) await payload.update({ collection: 'blocks-fields', id, locale: 'es', data: { content: [ { blockType: 'blockInsideBlock', array: [ { link: { label: 'Spanish 1', }, }, { link: { label: 'Spanish 2', }, }, ], content: [ { blockType: 'textBlock', text: 'hola', }, ], }, ], }, }) const retrieved = await payload.findByID({ collection: 'blocks-fields', id, locale: 'all', }) expect(retrieved.content.en[0].content).toHaveLength(1) expect(retrieved.content.es[0].content).toHaveLength(1) expect(retrieved.content.en[0].array[0].link.label).toStrictEqual('English 1') expect(retrieved.content.en[0].array[1].link.label).toStrictEqual('English 2') expect(retrieved.content.es[0].array[0].link.label).toStrictEqual('Spanish 1') expect(retrieved.content.es[0].array[1].link.label).toStrictEqual('Spanish 2') }) }) describe('nested arrays', () => { it('should not duplicate block rows for blocks within localized array fields', async () => { const randomDoc = ( await payload.find({ collection: 'localized-posts', depth: 0, }) ).docs[0] const randomDoc2 = ( await payload.find({ collection: 'localized-posts', depth: 0, }) ).docs[1] const blocksWithinArrayEN = [ { blockName: '1', blockType: 'someBlock', relationWithinBlock: randomDoc.id, myGroup: { text: 'hello in english 1', }, }, { blockName: '2', blockType: 'someBlock', relationWithinBlock: randomDoc.id, myGroup: { text: 'hello in english 2', }, }, { blockName: '3', blockType: 'someBlock', relationWithinBlock: randomDoc.id, myGroup: { text: 'hello in english 3', }, }, ] const blocksWithinArrayES = [ { blockName: '1', blockType: 'someBlock', relationWithinBlock: randomDoc2.id, myGroup: { text: 'hello in spanish 1', }, }, { blockName: '2', blockType: 'someBlock', relationWithinBlock: randomDoc2.id, myGroup: { text: 'hello in spanish 2', }, }, { blockName: '3', blockType: 'someBlock', relationWithinBlock: randomDoc2.id, myGroup: { text: 'hello in spanish 3', }, }, ] const createdEnDoc = await payload.create({ collection: 'nested-arrays', locale: 'en', depth: 0, data: { arrayWithBlocks: [ { blocksWithinArray: blocksWithinArrayEN as any, }, ], }, }) const updatedEsDoc = await payload.update({ collection: 'nested-arrays', id: createdEnDoc.id, depth: 0, locale: 'es', data: { arrayWithBlocks: [ { blocksWithinArray: blocksWithinArrayES as any, }, ], }, }) const esArrayBlocks = updatedEsDoc.arrayWithBlocks[0].blocksWithinArray // recursively remove any id field within esArrayRow const removeId = (obj) => { if (obj instanceof Object) { delete obj.id Object.values(obj).forEach(removeId) } } removeId(esArrayBlocks) removeId(createdEnDoc.arrayWithBlocks[0].blocksWithinArray) expect(esArrayBlocks).toEqual(blocksWithinArrayES) expect(createdEnDoc.arrayWithBlocks[0].blocksWithinArray).toEqual(blocksWithinArrayEN) // pull enDoc again and make sure the update of esDoc did not mess with the data of enDoc const enDoc2 = await payload.findByID({ id: createdEnDoc.id, collection: 'nested-arrays', locale: 'en', depth: 0, }) removeId(enDoc2.arrayWithBlocks[0].blocksWithinArray) expect(enDoc2.arrayWithBlocks[0].blocksWithinArray).toEqual(blocksWithinArrayEN) }) it('should update localized relation within unLocalized array', async () => { const randomTextDoc = ( await payload.find({ collection: 'localized-posts', depth: 0, }) ).docs[0] const randomTextDoc2 = ( await payload.find({ collection: 'localized-posts', depth: 0, }) ).docs[1] const createdEnDoc = await payload.create({ collection: 'nested-arrays', locale: 'en', depth: 0, data: { arrayWithLocalizedRelation: [ { localizedRelation: randomTextDoc.id, }, ], }, }) const updatedEsDoc = await payload.update({ collection: 'nested-arrays', id: createdEnDoc.id, depth: 0, locale: 'es', data: { arrayWithLocalizedRelation: [ { id: createdEnDoc.arrayWithLocalizedRelation[0].id, localizedRelation: randomTextDoc2.id, }, ], }, }) expect(updatedEsDoc.arrayWithLocalizedRelation).toHaveLength(1) expect(updatedEsDoc.arrayWithLocalizedRelation[0].localizedRelation).toBe(randomTextDoc2.id) expect(createdEnDoc.arrayWithLocalizedRelation).toHaveLength(1) expect(createdEnDoc.arrayWithLocalizedRelation[0].localizedRelation).toBe(randomTextDoc.id) // pull enDoc again and make sure the update of esDoc did not mess with the data of enDoc const enDoc2 = await payload.findByID({ id: createdEnDoc.id, collection: 'nested-arrays', locale: 'en', depth: 0, }) expect(enDoc2.arrayWithLocalizedRelation).toHaveLength(1) expect(enDoc2.arrayWithLocalizedRelation[0].localizedRelation).toBe(randomTextDoc.id) }) }) describe('nested fields', () => { it('should allow for fields which could contain new tables within localized arrays to be stored', async () => { const randomDoc = ( await payload.find({ collection: 'localized-posts', depth: 0, }) ).docs[0] const randomDoc2 = ( await payload.find({ collection: 'localized-posts', depth: 0, }) ).docs[1] const newDoc = await payload.create({ collection: 'nested-field-tables', data: { array: [ { relation: { value: randomDoc.id, relationTo: 'localized-posts', }, hasManyRelation: [randomDoc.id, randomDoc2.id], hasManyPolyRelation: [ { relationTo: 'localized-posts', value: randomDoc.id, }, { relationTo: 'localized-posts', value: randomDoc2.id, }, ], number: [1, 2], text: ['hello', 'goodbye'], select: ['one'], }, ], }, }) await payload.update({ collection: 'nested-field-tables', id: newDoc.id, locale: 'es', data: { array: [ { relation: { value: randomDoc2.id, relationTo: 'localized-posts', }, hasManyRelation: [randomDoc2.id, randomDoc.id], hasManyPolyRelation: [ { relationTo: 'localized-posts', value: randomDoc2.id, }, { relationTo: 'localized-posts', value: randomDoc.id, }, ], select: ['two', 'three'], text: ['hola', 'adios'], number: [3, 4], }, ], }, }) const retrieved = await payload.findByID({ collection: 'nested-field-tables', id: newDoc.id, depth: 0, locale: 'all', }) expect(retrieved.array.en[0].relation.value).toStrictEqual(randomDoc.id) expect(retrieved.array.es[0].relation.value).toStrictEqual(randomDoc2.id) expect(retrieved.array.en[0].hasManyRelation).toEqual([randomDoc.id, randomDoc2.id]) expect(retrieved.array.es[0].hasManyRelation).toEqual([randomDoc2.id, randomDoc.id]) expect(retrieved.array.en[0].hasManyPolyRelation).toEqual([ { value: randomDoc.id, relationTo: 'localized-posts' }, { value: randomDoc2.id, relationTo: 'localized-posts' }, ]) expect(retrieved.array.es[0].hasManyPolyRelation).toEqual([ { value: randomDoc2.id, relationTo: 'localized-posts' }, { value: randomDoc.id, relationTo: 'localized-posts' }, ]) expect(retrieved.array.en[0].number).toEqual([1, 2]) expect(retrieved.array.es[0].number).toEqual([3, 4]) expect(retrieved.array.en[0].select).toEqual(['one']) expect(retrieved.array.es[0].select).toEqual(['two', 'three']) expect(retrieved.array.en[0].text).toEqual(['hello', 'goodbye']) expect(retrieved.array.es[0].text).toEqual(['hola', 'adios']) }) it('should allow for relationship in new tables within blocks inside of localized blocks to be stored', async () => { const randomDoc = ( await payload.find({ collection: 'localized-posts', depth: 0, }) ).docs[0] const randomDoc2 = ( await payload.find({ collection: 'localized-posts', depth: 0, }) ).docs[1] const docEn = await payload.create({ collection: 'nested-field-tables', depth: 0, data: { blocks: [ { blockType: 'block', nestedBlocks: [ { blockType: 'content', relation: { relationTo: 'localized-posts', value: randomDoc.id, }, }, ], }, { blockType: 'block', nestedBlocks: [ { blockType: 'content', relation: { relationTo: 'localized-posts', value: randomDoc.id, }, }, ], }, { blockType: 'block', nestedBlocks: [ { blockType: 'content', relation: { relationTo: 'localized-posts', value: randomDoc.id, }, }, ], }, ], }, }) expect(docEn.blocks[0].nestedBlocks[0].relation.value).toBe(randomDoc.id) expect(docEn.blocks[1].nestedBlocks[0].relation.value).toBe(randomDoc.id) expect(docEn.blocks[2].nestedBlocks[0].relation.value).toBe(randomDoc.id) const docEs = await payload.update({ id: docEn.id, depth: 0, locale: 'es', collection: 'nested-field-tables', data: { blocks: [ { blockType: 'block', nestedBlocks: [ { blockType: 'content', relation: { relationTo: 'localized-posts', value: randomDoc2.id, }, }, ], }, { blockType: 'block', nestedBlocks: [ { blockType: 'content', relation: { relationTo: 'localized-posts', value: randomDoc2.id, }, }, ], }, { blockType: 'block', nestedBlocks: [ { blockType: 'content', relation: { relationTo: 'localized-posts', value: randomDoc2.id, }, }, ], }, ], }, }) expect(docEs.blocks[0].nestedBlocks[0].relation.value).toBe(randomDoc2.id) expect(docEs.blocks[1].nestedBlocks[0].relation.value).toBe(randomDoc2.id) expect(docEs.blocks[2].nestedBlocks[0].relation.value).toBe(randomDoc2.id) const docAll = await payload.findByID({ collection: 'nested-field-tables', id: docEn.id, locale: 'all', depth: 0, }) expect(docAll.blocks.en[0].nestedBlocks[0].relation.value).toBe(randomDoc.id) expect(docAll.blocks.en[1].nestedBlocks[0].relation.value).toBe(randomDoc.id) expect(docAll.blocks.en[2].nestedBlocks[0].relation.value).toBe(randomDoc.id) expect(docAll.blocks.es[0].nestedBlocks[0].relation.value).toBe(randomDoc2.id) expect(docAll.blocks.es[1].nestedBlocks[0].relation.value).toBe(randomDoc2.id) expect(docAll.blocks.es[2].nestedBlocks[0].relation.value).toBe(randomDoc2.id) }) it('should allow for relationship in new tables within arrays inside of localized blocks to be stored', async () => { const randomDoc = ( await payload.find({ collection: 'localized-posts', depth: 0, }) ).docs[0] const randomDoc2 = ( await payload.find({ collection: 'localized-posts', depth: 0, }) ).docs[1] const docEn = await payload.create({ collection: 'nested-field-tables', depth: 0, data: { blocks: [ { blockType: 'block', array: [ { relation: { relationTo: 'localized-posts', value: randomDoc.id, }, }, ], }, { blockType: 'block', array: [ { relation: { relationTo: 'localized-posts', value: randomDoc.id, }, }, ], }, { blockType: 'block', array: [ { relation: { relationTo: 'localized-posts', value: randomDoc.id, }, }, ], }, ], }, }) expect(docEn.blocks[0].array[0].relation.value).toBe(randomDoc.id) expect(docEn.blocks[1].array[0].relation.value).toBe(randomDoc.id) expect(docEn.blocks[2].array[0].relation.value).toBe(randomDoc.id) const docEs = await payload.update({ id: docEn.id, depth: 0, locale: 'es', collection: 'nested-field-tables', data: { blocks: [ { blockType: 'block', array: [ { relation: { relationTo: 'localized-posts', value: randomDoc2.id, }, }, ], }, { blockType: 'block', array: [ { relation: { relationTo: 'localized-posts', value: randomDoc2.id, }, }, ], }, { blockType: 'block', array: [ { relation: { relationTo: 'localized-posts', value: randomDoc2.id, }, }, ], }, ], }, }) expect(docEs.blocks[0].array[0].relation.value).toBe(randomDoc2.id) expect(docEs.blocks[1].array[0].relation.value).toBe(randomDoc2.id) expect(docEs.blocks[2].array[0].relation.value).toBe(randomDoc2.id) const docAll = await payload.findByID({ collection: 'nested-field-tables', id: docEn.id, locale: 'all', depth: 0, }) expect(docAll.blocks.en[0].array[0].relation.value).toBe(randomDoc.id) expect(docAll.blocks.en[1].array[0].relation.value).toBe(randomDoc.id) expect(docAll.blocks.en[2].array[0].relation.value).toBe(randomDoc.id) expect(docAll.blocks.es[0].array[0].relation.value).toBe(randomDoc2.id) expect(docAll.blocks.es[1].array[0].relation.value).toBe(randomDoc2.id) expect(docAll.blocks.es[2].array[0].relation.value).toBe(randomDoc2.id) }) }) describe('localized with unique', () => { it('localized with unique should work for each locale', async () => { await payload.create({ collection: 'localized-posts', locale: 'ar', data: { unique: 'text', }, }) await payload.create({ collection: 'localized-posts', locale: 'en', data: { unique: 'text', }, }) await payload.create({ collection: 'localized-posts', locale: 'es', data: { unique: 'text', }, }) await expect( payload.create({ collection: 'localized-posts', locale: 'en', data: { unique: 'text', }, }), ).rejects.toBeTruthy() }) }) describe('Copying To Locale', () => { let user: User beforeAll(async () => { user = ( await payload.find({ collection: 'users', where: { email: { equals: devUser.email, }, }, }) ).docs[0] as unknown as User user['collection'] = 'users' }) it('should copy to locale', async () => { const doc = await payload.create({ collection: 'localized-posts', data: { title: 'Hello', group: { children: 'Children', }, unique: 'unique-field', localizedCheckbox: true, }, }) const req = await createLocalReq({ user }, payload) const res = (await copyDataFromLocaleHandler({ fromLocale: 'en', req, toLocale: 'es', docID: doc.id, collectionSlug: 'localized-posts', })) as LocalizedPost expect(res.title).toBe('Hello') expect(res.group.children).toBe('Children') expect(res.unique).toBe('unique-field') expect(res.localizedCheckbox).toBe(true) }) it('should copy localized nested to arrays', async () => { const doc = await payload.create({ collection: 'nested', locale: 'en', data: { topLevelArray: [ { localizedText: 'some-localized-text', notLocalizedText: 'some-not-localized-text', }, ], }, }) const req = await createLocalReq({ user }, payload) const res = (await copyDataFromLocaleHandler({ fromLocale: 'en', req, toLocale: 'es', docID: doc.id, collectionSlug: 'nested', })) as Nested expect(res.topLevelArray[0].localizedText).toBe('some-localized-text') expect(res.topLevelArray[0].notLocalizedText).toBe('some-not-localized-text') }) it('should copy localized arrays', async () => { const doc = await payload.create({ collection: 'nested', locale: 'en', data: { topLevelArrayLocalized: [ { text: 'some-text', }, ], }, }) const req = await createLocalReq({ user }, payload) const res = (await copyDataFromLocaleHandler({ fromLocale: 'en', req, toLocale: 'es', docID: doc.id, collectionSlug: 'nested', })) as Nested expect(res.topLevelArrayLocalized[0].text).toBe('some-text') }) }) }) 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 [spanishLocale]: string } }): Promise { const localizedRelation: any = await payload.create({ collection, data: { title: data.title.en, }, }) await payload.update({ id: localizedRelation.id, collection, data: { title: data.title.es, }, locale: spanishLocale, }) return localizedRelation }